精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Python 源文件編譯之后會得到什么,它的結構是怎樣的?和字節碼又有什么聯系?

開發 前端
Python 的函數、類、模塊等,都具有各自的作用域,每個作用域對應一個獨立的代碼塊,在編譯時,Python 編譯器會為每個代碼塊都創建一個 PyCodeObject 對象。

楔子

當我們執行一個 py 文件的時候,只需要在命令行中輸入 python xxx.py 即可,但你有沒有想過這背后的流程是怎樣的呢?

首先 py 文件不是一上來就直接執行的,而是會先有一個編譯的過程,整個步驟如下:

圖片圖片

這里我們看到了 Python 編譯器、Python 虛擬機,而且我們平常還會說 Python 解釋器,那么三者之間有什么區別呢?

圖片圖片

Python 編譯器負責將 Python 源代碼編譯成 PyCodeObject 對象,然后交給 Python 虛擬機來執行。

那么 Python 編譯器和 Python 虛擬機都在什么地方呢?如果打開 Python 的安裝目錄,會發現有一個 python.exe,點擊的時候會通過它來啟動一個終端。

圖片

但問題是這個文件大小還不到 100K,不可能容納一個編譯器加一個虛擬機,所以下面還有一個 python312.dll。沒錯,編譯器、虛擬機都藏身于 python312.dll 當中。

因此 Python 雖然是解釋型語言,但也有編譯的過程。源代碼會被編譯器編譯成 PyCodeObject 對象,然后再交給虛擬機來執行。而之所以要存在編譯,是為了讓虛擬機能更快速地執行,比如在編譯階段常量都會提前分配好,而且還可以盡早檢測出語法上的錯誤。

pyc 文件是什么

在 Python 開發時,我們肯定都見過這個 pyc 文件,它一般位于 __pycache__ 目錄中,那么 pyc 文件和 PyCodeObject 之間有什么關系呢?

首先我們都知道字節碼,虛擬機的執行實際上就是對字節碼不斷解析的一個過程。然而除了字節碼之外,還應該包含一些其它的信息,這些信息也是 Python 運行的時候所必需的,比如常量、變量名等等。

我們常聽到 py 文件被編譯成字節碼,這句話其實不太嚴謹,因為字節碼只是一個 PyBytesObject 對象、或者說一段字節序列。但很明顯,光有字節碼是不夠的,還有很多的靜態信息也需要被收集起來,它們整體被稱為 PyCodeObject。

而 PyCodeObject 對象中有一個字段 co_code,它是一個指針,指向了這段字節序列。但是這個對象除了有 co_code 指向的字節碼之外,還有很多其它字段,負責保存代碼涉及到的常量、變量(名字、符號)等等。

所以雖然編寫的是 py 文件,但虛擬機執行的是編譯后的 PyCodeObject 對象。但是問題來了,難道每一次執行都要將源文件編譯一遍嗎?如果沒有對源文件進行修改的話,那么完全可以使用上一次的編譯結果。相信此時你能猜到 pyc 文件是干什么的了,它就是負責保存編譯之后的 PyCodeObject 對象。

現在我們知道了,pyc 文件里面保存的內容是 PyCodeObject 對象。對于 Python 編譯器來說,PyCodeObject 對象是對源代碼編譯之后的結果,而 pyc 文件則是這個對象在硬盤上的表現形式。

當下一次運行的時候,Python 解釋器會根據 pyc 文件中記錄的編譯結果,直接建立內存中的 PyCodeObject 對象,而不需要再重新編譯了,當然前提是沒有對源文件進行修改。

PyCodeObject 底層結構

既然 PyCodeObject 對象是源代碼的編譯結果,那么搞清楚它的底層結構就至關重要,下面來看一下它長什么樣子。相比以前的版本(比如 3.8),結構變化還是有一點大的。

// Include/pytypedefs.h
typedef struct PyCodeObject PyCodeObject;

// Include/cpython/code.h
struct PyCodeObject _PyCode_DEF(1);

#define _PyCode_DEF(SIZE) {                   \
    PyObject_VAR_HEAD                         \
                                              \
    PyObject *co_consts;                      \
    PyObject *co_names;                       \
    PyObject *co_exceptiontable;              \
    int co_flags;                             \
    int co_argcount;                          \
    int co_posonlyargcount;                   \
    int co_kwonlyargcount;                    \
    int co_stacksize;                         \
    int co_firstlineno;                       \
    int co_nlocalsplus;                       \
    int co_framesize;                         \
    int co_nlocals;                           \
    int co_ncellvars;                         \
    int co_nfreevars;                         \
    uint32_t co_version;                      \
    PyObject *co_localsplusnames;             \
    PyObject *co_localspluskinds;             \
    PyObject *co_filename;                    \
    PyObject *co_name;                        \
    PyObject *co_qualname;                    \
    PyObject *co_linetable;                   \
    PyObject *co_weakreflist;                 \
    _PyCoCached *_co_cached;                  \
    uint64_t _co_instrumentation_version;     \
    _PyCoMonitoringData *_co_monitoring;      \
    int _co_firsttraceable;                   \
    void *co_extra;                           \
    char co_code_adaptive[(SIZE)];            \
}

這里面的每一個字段,我們一會兒都會詳細介紹,并通過代碼逐一演示??傊?Python 編譯器在對源代碼進行編譯的時候,針對每一個 code block(代碼塊),都會創建一個 PyCodeObject 與之對應。

但多少代碼才算得上是一個 block 呢?事實上,Python 有一個簡單而清晰的規則:當進入一個新的名字空間,或者說作用域時,就算是進入了一個新的 block 了。舉個例子:

class A:
    a = 123

def foo():
    a = []

我們仔細觀察一下上面這段代碼,它在編譯完之后會有三個 PyCodeObject 對象,一個是對應整個 py 文件(模塊)的,一個是對應 class A 的,一個是對應 def foo 的。因為這是三個不同的作用域,所以會有三個 PyCodeObject 對象。

所以一個 code block 對應一個作用域、同時也對應一個 PyCodeObject 對象。Python 的類、函數、模塊都有自己獨立的作用域,因此在編譯時也都會有一個 PyCodeObject 對象與之對應。

PyCodeObject 字段解析

PyCodeObject 我們知道它是干什么的了,那如何才能拿到這個對象呢?首先該對象在 Python 里面的類型是 <class 'code'>,但是底層沒有將這個類暴露給我們,因此 code 這個名字在 Python 里面只是一個沒有定義的變量罷了。

但我們可以通過其它的方式進行獲取,比如函數。

def func():
    pass

print(func.__code__)  # <code object ......
print(type(func.__code__))  # <class 'code'>

我們可以通過函數的 __code__ 屬性拿到底層對應的 PyCodeObject 對象,當然也可以獲取里面的字段,我們來演示一下,并詳細介紹每個字段的含義。

PyObject_VAR_HEAD:變長對象的頭部信息

我們看到 Python 真的一切皆對象,源代碼編譯之后的結果也是一個對象。

co_consts:常量池,一個元組,保存代碼塊中創建的所有常量

def foo():
    a = 123
    b = "hello"
    c = (1, 2)
    d = ["x", "y"]
    e = {"p": "k"}
    f = {7, 8}

print(foo.__code__.co_consts)
"""
(None, 123, 'hello', (1, 2), 'x', 'y', 'p', 'k', 7, 8)
"""

co_consts 里面出現的都是編譯階段可以確定的常量,而 ["x", "y"] 和 {"p": "k"} 沒有出現,由此我們可以得出,列表和字典絕不是在編譯階段構建的。編譯時,只是收集了里面的元素,然后等到運行時再去動態構建。

不過問題來了,在構建的時候解釋器怎么知道是要構建列表、還是字典、亦或是其它的什么對象呢?所以這就依賴于字節碼了,解釋字節碼的時候,會判斷到底要構建什么樣的對象。

因此解釋器執行的是字節碼,核心邏輯都體現在字節碼中。但是光有字節碼還不夠,它包含的只是程序的主干邏輯,至于變量、常量,則從符號表和常量池里面獲取。

然后還有一個細節需要注意:

def foo():
    a = ["x", "y", "z"]
    b = {1, 2, 3}
    c = 3 + 4

print(foo.__code__.co_consts)
"""
(None, ('x', 'y', 'z'), frozenset({1, 2, 3}), 7)
"""

當列表的長度不小于 3 時,里面的元素如果都可以在編譯階段確定,那么整體會作為一個元組被收集起來,這樣多條字節碼可以合并為一條。集合也是類似的,里面的元素整體會作為一個不可變集合被收集起來。

圖片圖片

關于字節碼的更多細節,我們后續再聊。

另外函數里面的變量 c 等于 3 + 4,但常量池里面直接存儲了 7,這個過程叫做常量折疊。常量之間的加減乘除,結果依舊是一個常量,編譯階段就會計算好。

def foo():
    a = 1 + 3
    b = "hello" + " " + "world"
    c = ("a", "b") + ("c", "d")

print(foo.__code__.co_consts)
"""
(None, 4, 'hello world', ('a', 'b', 'c', 'd'))
"""

以上就是常量池,負責保存代碼塊中創建的所有常量。

co_names:符號表,一個元組,保存代碼塊中引用的其它作用域的變量

c = 1

def foo(a, b):
    print(a, b, c)
    d = (list, int, str)

print(foo.__code__.co_names)
"""
('print', 'c', 'list', 'int', 'str')
"""

雖然一切皆對象,但看到的都是指向對象的變量,所以 print, c, list, int, str 都是變量,它們都不在當前 foo 函數的作用域中。

co_exceptiontable:異常處理表

這個字段后續介紹異常處理的時候會細說,目前先有一個簡單的了解即可。當解釋器執行某個指令出現錯誤時,那么會引發一個異常,如果異常產生的位置位于 try 語句塊內,那么解釋器必須跳轉到相應的 except 或 finally 語句塊內,這是顯然的。

在 Python 3.10 以及之前的版本,這個機制是通過引入一個獨立的動態棧,然后跟蹤 try 語句塊實現的。但從 3.11 開始,動態棧被替換成了靜態表,這個表由 co_exceptiontable 字段維護,并在編譯期間就靜態生成了。

def foo():
    try:
        1 / 0
    except Exception:
        pass

print(foo.__code__.co_exceptiontable)
"""
b'\x82\x05\x08\x00\x88\t\x14\x03\x93\x01\x14\x03'
"""

異常處理表本質上是一段字節序列,因為是靜態數據,所以可以高效地讀取。這段字節序列里面包含了代碼塊中的 try / except / finally 信息,當代碼在執行過程中出現異常時,解釋器會查詢這張表,尋找與之匹配的 except 塊。

關于該字段的更多細節,我們后續介紹異常捕獲的時候細說,總之通過將動態棧換成靜態表,可以大幅提升解釋器在異常處理時的效率。

co_flags:函數標識

先來提出一個問題:

def some_func():
    return "hello world"

def some_gen():
    yield
    return "hello world"

print(some_func.__class__)
print(some_gen.__class__)
"""
<class 'function'>
<class 'function'>
"""

print(some_func())
"""
hello world
"""
print(some_gen())
"""
<generator object some_gen at 0x1028a80b0>
"""

調用 some_func 會將代碼執行完畢,調用 some_gen 會返回生成器,但問題是這兩者都是函數類型,為什么執行的時候會有不同的表現呢?

可能有人覺得這還不簡單,Python 具有詞法作用域,由于 some_func 里面沒有出現 yield 關鍵字,所以是普通函數,而 some_gen 里面出現了 yield,所以是生成器函數。

從源代碼來看確實如此,但源代碼是要編譯成 PyCodeObject 對象的,在編譯之后,函數內部是否出現 yield 關鍵字這一信息要怎么體現呢?答案便是通過 co_flags 字段。

然后解釋器內部定義了一系列的標志位,通過和 co_flags 字段按位與,便可判斷函數是否具備指定特征。常見的標志位如下:

// Include/cpython/code.h

// 函數參數是否包含 *args
#define CO_VARARGS      0x0004
// 函數參數是否包含 **kwargs
#define CO_VARKEYWORDS  0x0008
// 函數是否是內層函數
#define CO_NESTED       0x0010
// 函數是否是生成器函數
#define CO_GENERATOR    0x0020
// 函數是否是協程函數
#define CO_COROUTINE            0x0080
// 函數是否是異步生成器函數
#define CO_ASYNC_GENERATOR      0x0200

我們實際測試一下,比如檢測函數的參數類型:

CO_VARARGS = 0x0004
CO_VARKEYWORDS = 0x0008
CO_NESTED = 0x0010


def foo(*args):
    pass

def bar():
    pass

# 因為 foo 的參數包含 *args,所以和 CO_VARARGS 按位與的結果為真
# 而 bar 的參數不包含 *args,所以結果為假
print(foo.__code__.co_flags & CO_VARARGS)  # 4
print(bar.__code__.co_flags & CO_VARARGS)  # 0


def foo(**kwargs):
    pass

def bar():
    pass

print(foo.__code__.co_flags & CO_VARKEYWORDS)  # 8
print(bar.__code__.co_flags & CO_VARKEYWORDS)  # 0


def foo():
    def bar():
        pass
    return bar

# foo 是外層函數,所以和 CO_NESTED 按位與的結果為假
# foo() 返回的是內層函數,所以和 CO_NESTED 按位與的結果為真
print(foo.__code__.co_flags & CO_NESTED)  # 0
print(foo().__code__.co_flags & CO_NESTED)  # 16

當然啦,co_flags 還可以檢測一個函數的類型。比如函數內部出現了 yield,那么它就是一個生成器函數,調用之后可以得到一個生成器;使用 async def 定義,那么它就是一個協程函數,調用之后可以得到一個協程。

這些在詞法分析的時候就可以檢測出來,編譯之后會體現在 co_flags 字段中。

CO_GENERATOR = 0x0020
CO_COROUTINE = 0x0080
CO_ASYNC_GENERATOR = 0x0200

# 如果是生成器函數
# 那么 co_flags & 0x20 為真
def foo1():
    yield
print(foo1.__code__.co_flags & 0x20)  # 32

# 如果是協程函數
# 那么 co_flags & 0x80 為真
async def foo2():
    pass
print(foo2.__code__.co_flags & 0x80)  # 128
# 顯然 foo2 不是生成器函數
# 所以 co_flags & 0x20 為假
print(foo2.__code__.co_flags & 0x20)  # 0

# 如果是異步生成器函數
# 那么 co_flags & 0x200 為真
async def foo3():
    yield
print(foo3.__code__.co_flags & 0x200)  # 512
# 顯然它不是生成器函數、也不是協程函數
# 因此和 0x20、0x80 按位與之后,結果都為假
print(foo3.__code__.co_flags & 0x20)  # 0
print(foo3.__code__.co_flags & 0x80)  # 0

在判斷函數種類時,這種方式是最優雅的。

co_argcount:可以通過位置參數傳遞的參數個數

def foo(a, b, c=3):
    pass
print(foo.__code__.co_argcount)  # 3

def bar(a, b, *args):
    pass
print(bar.__code__.co_argcount)  # 2

def func(a, b, *args, c):
    pass
print(func.__code__.co_argcount)  # 2

函數 foo 中的參數 a、b、c 都可以通過位置參數傳遞,所以結果是 3。而函數 bar 則是兩個,這里不包括 *args。最后函數 func 顯然也是兩個,因為參數 c 只能通過關鍵字參數傳遞。

co_posonlyargcount:只能通過位置參數傳遞的參數個數,Python3.8 新增

def foo(a, b, c):
    pass
print(foo.__code__.co_posonlyargcount)  # 0

def bar(a, b, /, c):
    pass
print(bar.__code__.co_posonlyargcount)  # 2

注意:這里是只能通過位置參數傳遞的參數個數。對于 foo 而言,里面的三個參數既可以通過位置參數、也可以通過關鍵字參數傳遞,所以個數是 0。而函數 bar,里面的 a、b 只能通過位置參數傳遞,所以個數是 2。

co_kwonlyargcount:只能通過關鍵字參數傳遞的參數個數

def foo(a, b=1, c=2, *, d, e):
    pass
print(foo.__code__.co_kwonlyargcount)  # 2

這里是 d 和 e,它們必須通過關鍵字參數傳遞。

co_stacksize:執行該段代碼塊所需要的棧空間

def foo(a, b, c):
    name = "xxx"
    age = 16
    gender = "f"
    c = 33

print(foo.__code__.co_stacksize)  # 1

這個暫時不需要太關注,后續介紹棧幀的時候會詳細說明。

co_firstlineno:代碼塊的起始位置在源文件中的哪一行

def foo(a, b, c):
    pass

# 顯然是文件的第一行
# 或者理解為 def 所在的行
print(foo.__code__.co_firstlineno)  # 1

如果函數出現了調用呢?

def foo():
    return bar

def bar():
    pass

print(foo().__code__.co_firstlineno)  # 4

如果執行 foo,那么會返回函數 bar,因此結果是 def bar(): 所在的行數。所以每個函數都有自己的作用域,以及 PyCodeObject 對象。

_co_cached:結構體的倒數第六個字段,這里需要先拿出來解釋一下,它負責緩存以下字段

// Include/cpython/code.h
typedef struct {
    // 指令集,也就是字節碼,它是一個 bytes 對象 
    PyObject *_co_code;
    // 一個元組,保存當前作用域中創建的局部變量   
    PyObject *_co_varnames;
    // 一個元組,保存外層函數的作用域中被內層函數引用的變量
    PyObject *_co_cellvars;
    // 一個元組,保存內層函數引用的外層函數的作用域中的變量
    PyObject *_co_freevars;
} _PyCoCached;

在之前的版本中,這些字段都是直接單獨定義在 PyCodeObject 中,并且開頭也沒有下劃線。當然啦,如果是通過 Python 獲取的話,那么方式和之前一樣。

def foo(a, b, c):
    name = "satori"
    age = 16
    gender = "f"
    print(name, age, gender)

# 字節碼,一個 bytes 對象,它保存了要操作的指令
# 但光有字節碼是肯定不夠的,還需要其它的靜態信息
# 顯然這些信息連同字節碼一樣,都位于 PyCodeObject 中
print(foo.__code__.co_code)
"""
b'\x97\x00d\x01}\x03d\x02}\x04d\x03}\x05t\x01......'
"""
# 當前作用域中創建的變量,注意它和 co_names 的區別
# co_varnames 保存的是當前作用域中創建的局部變量
# 而 co_names 保存的是當前作用域中引用的其它作用域的變量
print(foo.__code__.co_varnames)
"""
('a', 'b', 'c', 'name', 'age', 'gender')
"""
print(foo.__code__.co_names)
"""
('print',)
"""

然后是 co_cellvars 和 co_freevars,看一下這兩個字段。

def foo(a, b, c):
    def bar():
        print(a, b, c)
    return bar

# co_cellvars:外層函數的作用域中被內層函數引用的變量
# co_freevars:內層函數引用的外層函數的作用域中的變量
print(foo.__code__.co_cellvars)
print(foo.__code__.co_freevars)
"""
('a', 'b', 'c')
()
"""
# foo 里面的變量 a、b、c 被內層函數 bar 引用了
# 所以它的 co_cellvars 是 ('a', 'b', 'c')
# 而 foo 不是內層函數,所以它的 co_freevars 是 ()

bar = foo(1, 2, 3)
print(bar.__code__.co_cellvars)
print(bar.__code__.co_freevars)
"""
()
('a', 'b', 'c')
"""
# bar 引用了外層函數 foo 里面的變量 a、b、c
# 所以它的 co_freevars 是 ('a', 'b', 'c')
# 而 bar 已經是最內層函數了,所以它的 co_cellvars 是 ()

當然目前的函數只嵌套了兩層,但嵌套三層甚至更多層也是一樣的。

def foo(a, b, c):
    def bar(d, e):
        print(a)
        def func():
            print(b, c, d, e)
        return func
    return bar

# 對于 foo 而言,它的內層函數就是 bar,至于最里面的 func
# 由于它定義在 bar 的內部,所以可以看做 bar 函數體的一部分
# 而 foo 里面的變量 a、b、c 都被內層函數引用了
print(foo.__code__.co_cellvars)  # ('a', 'b', 'c')
print(foo.__code__.co_freevars)  # ()

bar = foo(1, 2, 3)
# 對于函數 bar 而言,它的內層函數就是 func
# 而顯然 bar 里面的變量 d 和 e 被 func 引用了
print(bar.__code__.co_cellvars)  # ('d', 'e')
# 然后 bar 引用了外層函數 foo 里面的 a、b、c
print(bar.__code__.co_freevars)  # ('a', 'b', 'c')
# 所以 co_cellvars 和 co_freevars 這兩個字段的關系有點類似鏡像

co_cellvars 和 co_freevars 在后續介紹閉包的時候會用到,以上就是這幾個字段的含義。

co_nlocals:代碼塊中局部變量的個數,也包括參數

def foo(a, b, *args, c, **kwargs):
    name = "xxx"
    age = 16
    gender = "f"
    c = 33

print(foo.__code__.co_varnames)
"""
('a', 'b', 'c', 'args', 'kwargs', 'name', 'age', 'gender')
"""
print(foo.__code__.co_nlocals)
"""
8
"""

co_varnames 保存的是代碼塊的局部變量,顯然 co_nlocals 就是它的長度。并且我們看到在編譯之后,函數的局部變量就已經確定了,因為它們是靜態存儲的。

co_ncellvars:cell 變量的個數,即 co_cellvars 的長度

該字段解釋器沒有暴露出來。

co_nfreevars:free 變量的個數,即 co_freevars 的長度

該字段解釋器沒有暴露出來。

co_nlocalsplus:局部變量、cell 變量、free 變量的個數之和

該字段解釋器沒有暴露出來。

co_framesize:棧幀的大小

解釋器在將源代碼編譯成 PyCodeObject 之后,還要在此之上繼續創建 PyFrameObject 對象,即棧幀對象。也就是說,字節碼是在棧幀中被執行的,棧幀是虛擬機執行的上下文,局部變量、臨時變量、以及函數執行的相關信息都保存在棧幀中。

當然該字段解釋器也沒有暴露出來,我們后續會詳細討論它。

co_localsplusnames:一個元組,包含局部變量、cell 變量、free 變量,當然嚴謹的說法應該是變量的名稱

而上面的 co_nlocalsplus 字段便是 co_localsplusnames 的長度。

  • co_varnames:保存所有的局部變量;co_nlocals:局部變量的個數。
  • co_cellvars:保存所有的 cell 變量;co_ncellvars:cell 變量的個數;
  • co_freevars:保存所有的 free 變量;co_nfreevars:free 變量的個數;

所以可以得出如下結論:

圖片圖片

這個字段很重要,之后會反復用到。

co_localspluskinds:標識 co_localsplusnames 里面的每個變量的種類

我們說了,co_localsplusnames 里面包含了局部變量、cell 變量、free 變量的名稱,它們整體是作為一個元組存儲的。那么問題來了,當從 co_localsplusnames 里面獲取一個變量時,解釋器怎么知道這個變量是局部變量,還是 cell 變量或者 free 變量呢?

所以便有了 co_localspluskinds 字段,它是一段字節序列,一個字節對應一個變量。

// Include/internal/pycore_code.h
#define CO_FAST_HIDDEN  0x10  
#define CO_FAST_LOCAL   0x20  // 局部變量
#define CO_FAST_CELL    0x40  // cell 變量
#define CO_FAST_FREE    0x80  // free 變量

比如 co_localspluskinds[3] 等于 0x20,那么 co_localsplusnames[3] 對應的便是局部變量。這里可能有人好奇,CO_FAST_HIDDEN 表示的是啥?顧名思義,該宏對應的是隱藏變量,所謂隱藏變量指的就是那些在當前作用域中不可見的變量。

def foo():
    lst = [x for x in range(10)]

比如列表推導式里面的循環變量,它就是一個隱藏變量,生命周期只局限于列表解析式內部,不會泄露到當前的局部作用域中。但 Python2 是會泄露的,如果你還要維護 Python2 老項目的話,那么這里要多加注意。

圖片圖片

以上就是 co_localspluskinds 字段的作用。

co_filename:代碼塊所在的文件的路徑

# 文件名:main.py
def foo():
    pass


print(foo.__code__.co_filename)
"""
/Users/satori/Documents/testing_project/main.py
"""

如果你無法使用 IDE,那么便可通過該字段查看函數定義在哪個文件中。

co_name:代碼塊的名字

def foo():
    pass

print(foo.__code__.co_name)  # foo

對于函數來說,代碼塊的名字就是函數名。

co_qualname:代碼塊的全限定名

def foo():
    pass

class A:
    def foo(self):
        pass

print(foo.__code__.co_qualname)  # foo
print(A.foo.__code__.co_qualname)  # A.foo

# 如果是獲取 co_name 字段,那么打印的則都是 "foo"

如果是類的成員函數,那么會將類名一起返回。

co_linetable:存儲指令和源代碼行號之間的對應關系

PyCodeObject 是源代碼編譯之后的產物,雖然兩者的結構千差萬別,但體現出的信息是一致的。像源代碼具有行號,那么編譯成 PyCodeObject 之后,行號信息也應該要有專門的字段來維護,否則報錯時我們就無法快速定位到行號。

在 3.10 之前,行號信息由 co_lnotab 字段(一個字節序列)維護,并且保存的是增量信息,舉個例子。

def foo():
    name = "古明地覺"
    hobby = [
        "sing",
        "dance",
        "rap",
        "??"
    ]
    age = 16

我們通過 dis 模塊反編譯一下。

圖片圖片

第一列數字表示行號,第二列數字表示字節碼指令的偏移量,或者說指令在整個字節碼指令集中的索引。我們知道字節碼指令集就是一段字節序列,由 co_code 字段維護,并且每個指令都帶有一個參數,所以偏移量(索引)為 0 2 4 6 8 ··· 的字節便是指令,偏移量為 1 3 5 7 9 ··· 的字節表示參數。

關于反編譯的具體細節后續會說,總之一個字節碼指令就是一個八位整數。對于當前函數來說,它的字節碼偏移量和行號的對應關系如下:

圖片圖片

當偏移量為 0 時,證明還沒有進入到函數體,那么源代碼行號便是 def 關鍵字所在的行號。然后偏移量增加 2、行號增加 1,接著偏移量增加 4、行號增加 1、最后偏移量增加 8、行號增加 6。

那么 co_lnotab 便是 2 1 4 1 8 6,我們測試一下。

結果和我們分析的一樣,但 co_lnotab 字段是 3.10 之前的,現在已經被替換成了 co_linetable,并且包含了更多的信息。當然啦,在 Python 里面這兩個字段都是可以訪問的,盡管有一部分字段已經被移除了,但為了保證兼容性,底層依舊支持我們通過 Python 訪問。

co_weakreflist:弱引用列表

PyCodeObject 對象支持弱引用,弱引用它的 PyObject * 會保存在該列表中。

以上就是 PyCodeObject 里面的字段的含義,至于剩下的幾個字段目前先跳過,后續涉及到的時候再說。

圖片圖片

小結

  • Python 解釋器 = Python 編譯器 + Python 虛擬機。
  • 編譯器先將 .py 源碼文件編譯成 PyCodeObject 對象,然后再交給虛擬機執行。
  • PyCodeObject 對象可以認為是源碼文件的另一種等價形式,但經過編譯,虛擬機可以更快速地執行。
  • 為了避免每次都要對源文件進行編譯,因此編譯后的結果會序列化在 .pyc 文件中,如果源文件沒有做改動,那么下一次執行時會直接從 .pyc 中讀取。
  • Python 的函數、類、模塊等,都具有各自的作用域,每個作用域對應一個獨立的代碼塊,在編譯時,Python 編譯器會為每個代碼塊都創建一個 PyCodeObject 對象。

最后我們又詳細介紹了 PyCodeObject 里面的字段的含義,相比幾年前剖析的 Python3.8 版本的源碼,3.12 的改動還是比較大的,底層增加了不少字段,并且移除了部分字段。但對于 Python 使用者而言,還是和之前一樣,解釋器依舊將它們以 <class 'code'> 實例屬性的形式暴露了出來。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2024-10-28 12:06:09

2024-05-22 08:02:30

2009-09-08 18:02:20

CCNA用途

2015-09-18 13:08:36

更新RedstoneWindows 10

2020-04-21 12:09:47

JVM消化字節碼

2024-05-07 09:24:12

Python源碼Java

2024-11-25 12:20:00

Hystrix微服務架構

2020-08-10 15:48:01

Python輪子計算

2024-08-08 11:05:22

2022-02-24 23:37:19

區塊鏈錢包比特幣

2023-04-17 14:21:19

5G無線技術

2022-08-08 07:04:34

URLIPHTTP

2023-05-04 00:16:39

數字化轉型運營

2024-12-03 09:34:35

觀察者模 式編程Javav

2013-05-31 09:17:31

云計算云技術大數據

2024-07-30 14:01:51

Java字節碼JVM?

2018-03-22 14:47:13

容器開發人員筆記本

2023-11-07 08:00:00

Kubernetes

2022-08-26 16:32:08

云計算公有云私有云

2021-05-25 07:59:59

Linux運維Linux系統
點贊
收藏

51CTO技術棧公眾號

国产亚洲精品久久| 亚洲一区二区在线免费观看视频| 国产精品久久久久7777婷婷| 亚洲不卡的av| 亚洲综合色婷婷在线观看| 偷窥国产亚洲免费视频| 亚洲一区美女| 日韩中文字幕影院| 美女视频黄a大片欧美| 久久国产色av| 国产成人无码精品久久二区三| 亚洲成人高清| 色综合久久九月婷婷色综合| 亚洲天堂第一区| 国产原创av在线| 成人不卡免费av| 成人av电影天堂| 日本久久综合网| 精品动漫3d一区二区三区免费版 | 性猛交ⅹ×××乱大交| 啦啦啦中文在线观看日本| 国产欧美日韩一区二区三区在线观看| 国产精品免费视频一区二区| 91精品国产乱码久久久久| 性xx色xx综合久久久xx| 欧美激情视频在线免费观看 欧美视频免费一 | 俺去啦;欧美日韩| 丰满少妇一区二区| 都市激情久久| 制服丝袜av成人在线看| 日韩精品无码一区二区三区免费| 免费在线看污片| 日韩理论在线观看| 偷拍视频一区二区| 久草在现在线| 久久久久久久久久久黄色| 国产免费一区二区| 成人免费一级视频| 国产精品1区二区.| 亚洲自拍高清视频网站| 国产精品久久久久久久久久久久久久久久 | 国产精品普通话| 国产熟妇一区二区三区四区| 奶水喷射视频一区| 清纯唯美亚洲综合| 亚洲另类在线观看| 国产农村妇女毛片精品久久莱园子| 久久99精品久久久久久噜噜| 在线观看成人毛片| 欧美大片专区| 高清视频欧美一级| 日韩黄色精品视频| 一区二区日本视频| 青青精品视频播放| jizz国产在线| 奇米影视一区二区三区小说| 国产精品久久久久久久久久久新郎 | 高清中文字幕mv的电影| 中文字幕一区二区三区中文字幕 | 国产精品久久久久久久久久10秀| 色噜噜国产精品视频一区二区| 国产一二三四视频| 91精品蜜臀一区二区三区在线| 26uuu成人网一区二区三区| 91午夜理伦私人影院| 国产探花精品一区二区| 国产精品123| 国产麻豆日韩| 欧洲毛片在线| 中文av一区二区| 伊人久久婷婷色综合98网| 国产调教视频在线观看| 一区二区三区四区在线播放 | 欧美精品成人一区二区三区四区| 亚洲欧美日本一区二区三区| 国产精品亚洲一区二区在线观看| 欧美r级电影在线观看| 国模私拍在线观看| 国产伦精品一区二区三区视频 | 成人免费观看视频大全| 一区二区三区在线看| 久久久一本二本三本| 日韩不卡视频在线观看| 欧美一区日本一区韩国一区| 国内自拍偷拍视频| 一本色道久久综合亚洲精品酒店| 中文字幕日韩在线观看| 免费毛片在线播放免费| 久久精品人人| 91视频免费进入| 全色精品综合影院| 亚洲图片你懂的| 日韩avxxx| 国产亚洲久久| 精品一区电影国产| 亚洲熟女www一区二区三区| 亚洲黄色天堂| 国产精品美女在线| 日本免费网站在线观看| 国产农村妇女精品| 男女私大尺度视频| 高清一区二区| 亚洲欧美国产一区二区三区| 亚洲欧美小视频| 久久蜜桃精品| 国产精品一区二区三区在线观| 日韩电影在线观看完整版| 亚洲人成7777| 91淫黄看大片| 黄色欧美在线| 欧美精品中文字幕一区| 日韩国产亚洲欧美| 99热99精品| 玖玖精品在线视频| 成人久久网站| 亚洲欧洲日产国码av系列天堂| 欧产日产国产v| 久久国产乱子精品免费女| 加勒比在线一区二区三区观看| 免费网站看v片在线a| 色网站国产精品| 日本黄色免费观看| 欧美三区视频| 亚洲a区在线视频| www.av在线播放| 色综合激情久久| 青青草视频网站| 国内精品美女在线观看| 91在线精品播放| 日本暖暖在线视频| 欧美性xxxxxxxx| 三上悠亚ssⅰn939无码播放| 亚洲看片一区| 国产精品一区二区你懂得| 综合图区亚洲| 91精品国产乱| 精品无码久久久久成人漫画| 蜜桃视频一区二区三区在线观看| 欧美中日韩免费视频| 色在线中文字幕| 亚洲国产精品高清久久久| 久久午夜无码鲁丝片| 国产精品69毛片高清亚洲| 国产又粗又大又爽的视频| 日本精品久久| 久久天天躁日日躁| 国产精品久久久久久久久毛片| 国产精品网曝门| 一级黄色录像在线观看| 天天超碰亚洲| av激情久久| av第一福利在线导航| 亚洲国产精品美女| 成人免费a视频| 久久久久久久久久看片| 久久精品影视大全| 欧美激情黄色片| 91精品视频一区| 午夜小视频在线观看| 欧美成人一区二区三区片免费| 久久丫精品久久丫| 91美女精品福利| 在线观看免费成人av| 日韩一区欧美| aa日韩免费精品视频一| 女海盗2成人h版中文字幕| 亚洲美女在线观看| 6—12呦国产精品| 亚洲在线视频免费观看| 国产精品无码电影| 日韩av在线播放中文字幕| 婷婷久久青草热一区二区| 国产亚洲高清在线观看| 97精品国产aⅴ7777| 黄色国产在线| 91精品国产综合久久蜜臀| 久久在线视频精品| 国产性色一区二区| 69久久精品无码一区二区| 一本色道久久综合| 视频一区视频二区视频三区视频四区国产 | 日本美女在线中文版| 欧美电视剧在线看免费| 日韩中文字幕在线观看视频| 国产精品国产三级国产普通话三级| 三日本三级少妇三级99| 国产欧美精品久久| 一区二区免费在线视频| 国产精品一区二区三区美女| 国产精品免费久久久久影院| 亚洲性图自拍| 亚洲性线免费观看视频成熟| www.蜜臀av.com| 在线视频你懂得一区| 福利所第一导航| 国产午夜亚洲精品理论片色戒| 永久av免费在线观看| 久久精品在线| 欧妇女乱妇女乱视频| 欧美日韩水蜜桃| 久久99精品久久久久久久久久| 精品久久在线| 欧美自拍大量在线观看| 91蜜桃在线视频| 在线成人激情黄色| 午夜在线观看视频18| 777奇米成人网| 销魂美女一区二区| 午夜精品久久久久久久久久久| 九九九视频在线观看| 97久久人人超碰| 男插女视频网站| 久久精品国产成人一区二区三区 | 青娱乐国产盛宴| 国产精品乱码人人做人人爱| 一区二区三区少妇| 国产成人aaa| 欧洲在线免费视频| 欧美a级一区二区| 国产一区二区三区精彩视频| 欧美激情日韩| 蜜桃视频成人在线观看| 欧美亚洲国产一区| 欧美自拍资源在线| 欧美a一欧美| 国产在线观看一区| swag国产精品一区二区| 99re在线国产| 成人在线视频www| 成人激情av在线| 欧洲午夜精品| 国产欧美日韩中文字幕| jvid一区二区三区| 国产成人午夜视频网址| 卡通欧美亚洲| 日本高清视频精品| 新版的欧美在线视频| 91精品国产91久久久| 成人性生交大片免费看网站| 欧美大片在线看| 午夜羞羞小视频在线观看| 欧美麻豆久久久久久中文| 精品国产99久久久久久| 久久精品99国产精品酒店日本| 日韩黄色影院| 久久激情视频免费观看| 99在线播放| 色综合视频一区中文字幕| 呦呦在线视频| 国产69精品久久久久9999| av最新在线| 55夜色66夜色国产精品视频| 欧美momandson| 国产精品久久久久久久久| 欧美天堂一区二区| 亚洲aⅴ男人的天堂在线观看| 国产一区二区三区亚洲综合| 亚洲综合视频1区| 97色成人综合网站| 精品乱色一区二区中文字幕| 欧美男gay| 亚洲欧美日韩在线综合| 亚洲精品久久久| 青青青青在线视频| 翔田千里一区二区| 婷婷激情四射五月天| 国产一区福利在线| 稀缺小u女呦精品呦| 2023国产精品| 婷婷国产成人精品视频| 一区二区三区精品| 五月婷婷开心网| 欧美日韩一区三区四区| www.中文字幕| 亚洲美女av电影| 蜜桃视频网站在线观看| 欧美激情欧美激情在线五月| 在线人成日本视频| 国产精品久久中文| 亚洲综合色婷婷在线观看| 免费av在线一区二区| 五月婷婷亚洲| 免费无码不卡视频在线观看| 日韩高清在线不卡| 中文字幕无人区二| 久久精品免费在线观看| 日韩精品一区二区亚洲av性色| 亚洲综合男人的天堂| 欧美a视频在线观看| 日韩一区二区三区视频在线观看| 亚洲人午夜射精精品日韩| 色七七影院综合| 美女露胸视频在线观看| 91色在线视频| 伊人久久大香线蕉综合网蜜芽| 国产成人精品免费看在线播放| 亚洲久久在线| 人妻精品久久久久中文字幕69| 久久理论电影网| 九九视频在线观看| 欧美四级电影在线观看| 午夜福利一区二区三区| 久久综合色影院| 日韩欧美精品一区二区综合视频| www.久久草| 99精品视频在线| 国产1区2区在线| 成人性生交大片免费| 小嫩苞一区二区三区| 色综合中文综合网| 高h放荡受浪受bl| 久久精品中文字幕电影| 免费在线观看一区| 蜜桃网站成人| 日韩午夜av在线| 无码国产精品久久一区免费| 中文一区一区三区高中清不卡| 久久不卡免费视频| 亚洲国产精品va| 女囚岛在线观看| 91网站在线看| 久久久久免费av| 国产福利在线免费| 国产女人18毛片水真多成人如厕| 国产精品美女久久久久av爽| 7777精品伊人久久久大香线蕉经典版下载 | 特级做a爱片免费69| 亚洲黄色有码视频| 欧美卡一卡二| 成人免费视频网站| 欧美不卡视频| 亚洲综合中文网| 亚洲激情自拍视频| 国产富婆一级全黄大片| 久久久成人精品| av在线亚洲一区| 亚洲成人动漫在线| 国产美女一区二区| 青青青在线免费观看| 5566中文字幕一区二区电影| 91高清在线视频| 国产在线观看精品| 我不卡手机影院| 夜夜爽久久精品91| 一区二区三区 在线观看视频| 99精品视频免费看| 久久99亚洲热视| 久久久久影视| av动漫在线观看| 久久色视频免费观看| 日韩三级一区二区| 亚洲一区999| 欧美综合影院| a级片一区二区| 成人av在线资源| 亚洲精品男人的天堂| 亚洲视频999| 涩涩涩久久久成人精品 | 一本综合久久| 精品人妻少妇嫩草av无码| 91久久国产最好的精华液| 成a人片在线观看www视频| 国产精品一二区| 欧美国产另类| 成年人小视频在线观看| 欧美日韩一区二区三区| 粉嫩一区二区三区国产精品| 国产欧美亚洲精品| 牛牛国产精品| 精品中文字幕在线播放| 欧美色播在线播放| 亚洲精品承认| 亚洲最大激情中文字幕| 亚洲国产一区二区精品专区| 蜜桃精品一区二区| 欧美人妖巨大在线| 狂野欧美性猛交xxxxx视频| 免费成人在线观看av| 精品一区二区免费看| 国产一级片免费看| 国产一区二区三区丝袜 | 久久综合一区| 久久99九九99精品| 97超碰人人干| 久久韩剧网电视剧| 亚洲欧美tv| 久久久久久久久久毛片| 丰满岳妇乱一区二区三区| 欧美日韩视频在线播放| 国产精品青青草| 免费不卡在线视频| 日本熟妇毛茸茸丰满| 在线看欧美日韩| 国产色噜噜噜91在线精品| www.天天射.com| 午夜国产不卡在线观看视频| 日本三级视频在线观看| 美女一区视频| 国产成人午夜电影网| 中文字幕视频在线播放|