成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Python 源文件編譯之后會(huì)得到什么,它的結(jié)構(gòu)是怎樣的?和字節(jié)碼又有什么聯(lián)系?

開發(fā) 前端
Python 的函數(shù)、類、模塊等,都具有各自的作用域,每個(gè)作用域?qū)?yīng)一個(gè)獨(dú)立的代碼塊,在編譯時(shí),Python 編譯器會(huì)為每個(gè)代碼塊都創(chuàng)建一個(gè) PyCodeObject 對(duì)象。

楔子

當(dāng)我們執(zhí)行一個(gè) py 文件的時(shí)候,只需要在命令行中輸入 python xxx.py 即可,但你有沒(méi)有想過(guò)這背后的流程是怎樣的呢?

首先 py 文件不是一上來(lái)就直接執(zhí)行的,而是會(huì)先有一個(gè)編譯的過(guò)程,整個(gè)步驟如下:

圖片圖片

這里我們看到了 Python 編譯器、Python 虛擬機(jī),而且我們平常還會(huì)說(shuō) Python 解釋器,那么三者之間有什么區(qū)別呢?

圖片圖片

Python 編譯器負(fù)責(zé)將 Python 源代碼編譯成 PyCodeObject 對(duì)象,然后交給 Python 虛擬機(jī)來(lái)執(zhí)行。

那么 Python 編譯器和 Python 虛擬機(jī)都在什么地方呢?如果打開 Python 的安裝目錄,會(huì)發(fā)現(xiàn)有一個(gè) python.exe,點(diǎn)擊的時(shí)候會(huì)通過(guò)它來(lái)啟動(dòng)一個(gè)終端。

圖片

但問(wèn)題是這個(gè)文件大小還不到 100K,不可能容納一個(gè)編譯器加一個(gè)虛擬機(jī),所以下面還有一個(gè) python312.dll。沒(méi)錯(cuò),編譯器、虛擬機(jī)都藏身于 python312.dll 當(dāng)中。

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

pyc 文件是什么

在 Python 開發(fā)時(shí),我們肯定都見過(guò)這個(gè) pyc 文件,它一般位于 __pycache__ 目錄中,那么 pyc 文件和 PyCodeObject 之間有什么關(guān)系呢?

首先我們都知道字節(jié)碼,虛擬機(jī)的執(zhí)行實(shí)際上就是對(duì)字節(jié)碼不斷解析的一個(gè)過(guò)程。然而除了字節(jié)碼之外,還應(yīng)該包含一些其它的信息,這些信息也是 Python 運(yùn)行的時(shí)候所必需的,比如常量、變量名等等。

我們常聽到 py 文件被編譯成字節(jié)碼,這句話其實(shí)不太嚴(yán)謹(jǐn),因?yàn)樽止?jié)碼只是一個(gè) PyBytesObject 對(duì)象、或者說(shuō)一段字節(jié)序列。但很明顯,光有字節(jié)碼是不夠的,還有很多的靜態(tài)信息也需要被收集起來(lái),它們整體被稱為 PyCodeObject。

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

所以雖然編寫的是 py 文件,但虛擬機(jī)執(zhí)行的是編譯后的 PyCodeObject 對(duì)象。但是問(wèn)題來(lái)了,難道每一次執(zhí)行都要將源文件編譯一遍嗎?如果沒(méi)有對(duì)源文件進(jìn)行修改的話,那么完全可以使用上一次的編譯結(jié)果。相信此時(shí)你能猜到 pyc 文件是干什么的了,它就是負(fù)責(zé)保存編譯之后的 PyCodeObject 對(duì)象。

現(xiàn)在我們知道了,pyc 文件里面保存的內(nèi)容是 PyCodeObject 對(duì)象。對(duì)于 Python 編譯器來(lái)說(shuō),PyCodeObject 對(duì)象是對(duì)源代碼編譯之后的結(jié)果,而 pyc 文件則是這個(gè)對(duì)象在硬盤上的表現(xiàn)形式。

當(dāng)下一次運(yùn)行的時(shí)候,Python 解釋器會(huì)根據(jù) pyc 文件中記錄的編譯結(jié)果,直接建立內(nèi)存中的 PyCodeObject 對(duì)象,而不需要再重新編譯了,當(dāng)然前提是沒(méi)有對(duì)源文件進(jìn)行修改。

PyCodeObject 底層結(jié)構(gòu)

既然 PyCodeObject 對(duì)象是源代碼的編譯結(jié)果,那么搞清楚它的底層結(jié)構(gòu)就至關(guān)重要,下面來(lái)看一下它長(zhǎng)什么樣子。相比以前的版本(比如 3.8),結(jié)構(gòu)變化還是有一點(diǎn)大的。

// 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)];            \
}

這里面的每一個(gè)字段,我們一會(huì)兒都會(huì)詳細(xì)介紹,并通過(guò)代碼逐一演示。總之 Python 編譯器在對(duì)源代碼進(jìn)行編譯的時(shí)候,針對(duì)每一個(gè) code block(代碼塊),都會(huì)創(chuàng)建一個(gè) PyCodeObject 與之對(duì)應(yīng)。

但多少代碼才算得上是一個(gè) block 呢?事實(shí)上,Python 有一個(gè)簡(jiǎn)單而清晰的規(guī)則:當(dāng)進(jìn)入一個(gè)新的名字空間,或者說(shuō)作用域時(shí),就算是進(jìn)入了一個(gè)新的 block 了。舉個(gè)例子:

class A:
    a = 123

def foo():
    a = []

我們仔細(xì)觀察一下上面這段代碼,它在編譯完之后會(huì)有三個(gè) PyCodeObject 對(duì)象,一個(gè)是對(duì)應(yīng)整個(gè) py 文件(模塊)的,一個(gè)是對(duì)應(yīng) class A 的,一個(gè)是對(duì)應(yīng) def foo 的。因?yàn)檫@是三個(gè)不同的作用域,所以會(huì)有三個(gè) PyCodeObject 對(duì)象。

所以一個(gè) code block 對(duì)應(yīng)一個(gè)作用域、同時(shí)也對(duì)應(yīng)一個(gè) PyCodeObject 對(duì)象。Python 的類、函數(shù)、模塊都有自己獨(dú)立的作用域,因此在編譯時(shí)也都會(huì)有一個(gè) PyCodeObject 對(duì)象與之對(duì)應(yīng)。

PyCodeObject 字段解析

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

但我們可以通過(guò)其它的方式進(jìn)行獲取,比如函數(shù)。

def func():
    pass

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

我們可以通過(guò)函數(shù)的 __code__ 屬性拿到底層對(duì)應(yīng)的 PyCodeObject 對(duì)象,當(dāng)然也可以獲取里面的字段,我們來(lái)演示一下,并詳細(xì)介紹每個(gè)字段的含義。

PyObject_VAR_HEAD:變長(zhǎng)對(duì)象的頭部信息

我們看到 Python 真的一切皆對(duì)象,源代碼編譯之后的結(jié)果也是一個(gè)對(duì)象。

co_consts:常量池,一個(gè)元組,保存代碼塊中創(chuàng)建的所有常量

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 里面出現(xiàn)的都是編譯階段可以確定的常量,而 ["x", "y"] 和 {"p": "k"} 沒(méi)有出現(xiàn),由此我們可以得出,列表和字典絕不是在編譯階段構(gòu)建的。編譯時(shí),只是收集了里面的元素,然后等到運(yùn)行時(shí)再去動(dòng)態(tài)構(gòu)建。

不過(guò)問(wèn)題來(lái)了,在構(gòu)建的時(shí)候解釋器怎么知道是要構(gòu)建列表、還是字典、亦或是其它的什么對(duì)象呢?所以這就依賴于字節(jié)碼了,解釋字節(jié)碼的時(shí)候,會(huì)判斷到底要構(gòu)建什么樣的對(duì)象。

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

然后還有一個(gè)細(xì)節(jié)需要注意:

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)
"""

當(dāng)列表的長(zhǎng)度不小于 3 時(shí),里面的元素如果都可以在編譯階段確定,那么整體會(huì)作為一個(gè)元組被收集起來(lái),這樣多條字節(jié)碼可以合并為一條。集合也是類似的,里面的元素整體會(huì)作為一個(gè)不可變集合被收集起來(lái)。

圖片圖片

關(guān)于字節(jié)碼的更多細(xì)節(jié),我們后續(xù)再聊。

另外函數(shù)里面的變量 c 等于 3 + 4,但常量池里面直接存儲(chǔ)了 7,這個(gè)過(guò)程叫做常量折疊。常量之間的加減乘除,結(jié)果依舊是一個(gè)常量,編譯階段就會(huì)計(jì)算好。

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'))
"""

以上就是常量池,負(fù)責(zé)保存代碼塊中創(chuàng)建的所有常量。

co_names:符號(hào)表,一個(gè)元組,保存代碼塊中引用的其它作用域的變量

c = 1

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

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

雖然一切皆對(duì)象,但看到的都是指向?qū)ο蟮淖兞浚?print, c, list, int, str 都是變量,它們都不在當(dāng)前 foo 函數(shù)的作用域中。

co_exceptiontable:異常處理表

這個(gè)字段后續(xù)介紹異常處理的時(shí)候會(huì)細(xì)說(shuō),目前先有一個(gè)簡(jiǎn)單的了解即可。當(dāng)解釋器執(zhí)行某個(gè)指令出現(xiàn)錯(cuò)誤時(shí),那么會(huì)引發(fā)一個(gè)異常,如果異常產(chǎn)生的位置位于 try 語(yǔ)句塊內(nèi),那么解釋器必須跳轉(zhuǎn)到相應(yīng)的 except 或 finally 語(yǔ)句塊內(nèi),這是顯然的。

在 Python 3.10 以及之前的版本,這個(gè)機(jī)制是通過(guò)引入一個(gè)獨(dú)立的動(dòng)態(tài)棧,然后跟蹤 try 語(yǔ)句塊實(shí)現(xiàn)的。但從 3.11 開始,動(dòng)態(tài)棧被替換成了靜態(tài)表,這個(gè)表由 co_exceptiontable 字段維護(hù),并在編譯期間就靜態(tài)生成了。

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'
"""

異常處理表本質(zhì)上是一段字節(jié)序列,因?yàn)槭庆o態(tài)數(shù)據(jù),所以可以高效地讀取。這段字節(jié)序列里面包含了代碼塊中的 try / except / finally 信息,當(dāng)代碼在執(zhí)行過(guò)程中出現(xiàn)異常時(shí),解釋器會(huì)查詢這張表,尋找與之匹配的 except 塊。

關(guān)于該字段的更多細(xì)節(jié),我們后續(xù)介紹異常捕獲的時(shí)候細(xì)說(shuō),總之通過(guò)將動(dòng)態(tài)棧換成靜態(tài)表,可以大幅提升解釋器在異常處理時(shí)的效率。

co_flags:函數(shù)標(biāo)識(shí)

先來(lái)提出一個(gè)問(wèn)題:

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>
"""

調(diào)用 some_func 會(huì)將代碼執(zhí)行完畢,調(diào)用 some_gen 會(huì)返回生成器,但問(wèn)題是這兩者都是函數(shù)類型,為什么執(zhí)行的時(shí)候會(huì)有不同的表現(xiàn)呢?

可能有人覺(jué)得這還不簡(jiǎn)單,Python 具有詞法作用域,由于 some_func 里面沒(méi)有出現(xiàn) yield 關(guān)鍵字,所以是普通函數(shù),而 some_gen 里面出現(xiàn)了 yield,所以是生成器函數(shù)。

從源代碼來(lái)看確實(shí)如此,但源代碼是要編譯成 PyCodeObject 對(duì)象的,在編譯之后,函數(shù)內(nèi)部是否出現(xiàn) yield 關(guān)鍵字這一信息要怎么體現(xiàn)呢?答案便是通過(guò) co_flags 字段。

然后解釋器內(nèi)部定義了一系列的標(biāo)志位,通過(guò)和 co_flags 字段按位與,便可判斷函數(shù)是否具備指定特征。常見的標(biāo)志位如下:

// Include/cpython/code.h

// 函數(shù)參數(shù)是否包含 *args
#define CO_VARARGS      0x0004
// 函數(shù)參數(shù)是否包含 **kwargs
#define CO_VARKEYWORDS  0x0008
// 函數(shù)是否是內(nèi)層函數(shù)
#define CO_NESTED       0x0010
// 函數(shù)是否是生成器函數(shù)
#define CO_GENERATOR    0x0020
// 函數(shù)是否是協(xié)程函數(shù)
#define CO_COROUTINE            0x0080
// 函數(shù)是否是異步生成器函數(shù)
#define CO_ASYNC_GENERATOR      0x0200

我們實(shí)際測(cè)試一下,比如檢測(cè)函數(shù)的參數(shù)類型:

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


def foo(*args):
    pass

def bar():
    pass

# 因?yàn)?foo 的參數(shù)包含 *args,所以和 CO_VARARGS 按位與的結(jié)果為真
# 而 bar 的參數(shù)不包含 *args,所以結(jié)果為假
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 是外層函數(shù),所以和 CO_NESTED 按位與的結(jié)果為假
# foo() 返回的是內(nèi)層函數(shù),所以和 CO_NESTED 按位與的結(jié)果為真
print(foo.__code__.co_flags & CO_NESTED)  # 0
print(foo().__code__.co_flags & CO_NESTED)  # 16

當(dāng)然啦,co_flags 還可以檢測(cè)一個(gè)函數(shù)的類型。比如函數(shù)內(nèi)部出現(xiàn)了 yield,那么它就是一個(gè)生成器函數(shù),調(diào)用之后可以得到一個(gè)生成器;使用 async def 定義,那么它就是一個(gè)協(xié)程函數(shù),調(diào)用之后可以得到一個(gè)協(xié)程。

這些在詞法分析的時(shí)候就可以檢測(cè)出來(lái),編譯之后會(huì)體現(xiàn)在 co_flags 字段中。

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

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

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

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

在判斷函數(shù)種類時(shí),這種方式是最優(yōu)雅的。

co_argcount:可以通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù)

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

函數(shù) foo 中的參數(shù) a、b、c 都可以通過(guò)位置參數(shù)傳遞,所以結(jié)果是 3。而函數(shù) bar 則是兩個(gè),這里不包括 *args。最后函數(shù) func 顯然也是兩個(gè),因?yàn)閰?shù) c 只能通過(guò)關(guān)鍵字參數(shù)傳遞。

co_posonlyargcount:只能通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù),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

注意:這里是只能通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù)。對(duì)于 foo 而言,里面的三個(gè)參數(shù)既可以通過(guò)位置參數(shù)、也可以通過(guò)關(guān)鍵字參數(shù)傳遞,所以個(gè)數(shù)是 0。而函數(shù) bar,里面的 a、b 只能通過(guò)位置參數(shù)傳遞,所以個(gè)數(shù)是 2。

co_kwonlyargcount:只能通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù)個(gè)數(shù)

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

這里是 d 和 e,它們必須通過(guò)關(guān)鍵字參數(shù)傳遞。

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

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

print(foo.__code__.co_stacksize)  # 1

這個(gè)暫時(shí)不需要太關(guān)注,后續(xù)介紹棧幀的時(shí)候會(huì)詳細(xì)說(shuō)明。

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

def foo(a, b, c):
    pass

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

如果函數(shù)出現(xiàn)了調(diào)用呢?

def foo():
    return bar

def bar():
    pass

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

如果執(zhí)行 foo,那么會(huì)返回函數(shù) bar,因此結(jié)果是 def bar(): 所在的行數(shù)。所以每個(gè)函數(shù)都有自己的作用域,以及 PyCodeObject 對(duì)象。

_co_cached:結(jié)構(gòu)體的倒數(shù)第六個(gè)字段,這里需要先拿出來(lái)解釋一下,它負(fù)責(zé)緩存以下字段

// Include/cpython/code.h
typedef struct {
    // 指令集,也就是字節(jié)碼,它是一個(gè) bytes 對(duì)象 
    PyObject *_co_code;
    // 一個(gè)元組,保存當(dāng)前作用域中創(chuàng)建的局部變量   
    PyObject *_co_varnames;
    // 一個(gè)元組,保存外層函數(shù)的作用域中被內(nèi)層函數(shù)引用的變量
    PyObject *_co_cellvars;
    // 一個(gè)元組,保存內(nèi)層函數(shù)引用的外層函數(shù)的作用域中的變量
    PyObject *_co_freevars;
} _PyCoCached;

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

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

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

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

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

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

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

當(dāng)然目前的函數(shù)只嵌套了兩層,但嵌套三層甚至更多層也是一樣的。

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

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

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

co_cellvars 和 co_freevars 在后續(xù)介紹閉包的時(shí)候會(huì)用到,以上就是這幾個(gè)字段的含義。

co_nlocals:代碼塊中局部變量的個(gè)數(shù),也包括參數(shù)

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 就是它的長(zhǎng)度。并且我們看到在編譯之后,函數(shù)的局部變量就已經(jīng)確定了,因?yàn)樗鼈兪庆o態(tài)存儲(chǔ)的。

co_ncellvars:cell 變量的個(gè)數(shù),即 co_cellvars 的長(zhǎng)度

該字段解釋器沒(méi)有暴露出來(lái)。

co_nfreevars:free 變量的個(gè)數(shù),即 co_freevars 的長(zhǎng)度

該字段解釋器沒(méi)有暴露出來(lái)。

co_nlocalsplus:局部變量、cell 變量、free 變量的個(gè)數(shù)之和

該字段解釋器沒(méi)有暴露出來(lái)。

co_framesize:棧幀的大小

解釋器在將源代碼編譯成 PyCodeObject 之后,還要在此之上繼續(xù)創(chuàng)建 PyFrameObject 對(duì)象,即棧幀對(duì)象。也就是說(shuō),字節(jié)碼是在棧幀中被執(zhí)行的,棧幀是虛擬機(jī)執(zhí)行的上下文,局部變量、臨時(shí)變量、以及函數(shù)執(zhí)行的相關(guān)信息都保存在棧幀中。

當(dāng)然該字段解釋器也沒(méi)有暴露出來(lái),我們后續(xù)會(huì)詳細(xì)討論它。

co_localsplusnames:一個(gè)元組,包含局部變量、cell 變量、free 變量,當(dāng)然嚴(yán)謹(jǐn)?shù)恼f(shuō)法應(yīng)該是變量的名稱

而上面的 co_nlocalsplus 字段便是 co_localsplusnames 的長(zhǎng)度。

  • co_varnames:保存所有的局部變量;co_nlocals:局部變量的個(gè)數(shù)。
  • co_cellvars:保存所有的 cell 變量;co_ncellvars:cell 變量的個(gè)數(shù);
  • co_freevars:保存所有的 free 變量;co_nfreevars:free 變量的個(gè)數(shù);

所以可以得出如下結(jié)論:

圖片圖片

這個(gè)字段很重要,之后會(huì)反復(fù)用到。

co_localspluskinds:標(biāo)識(shí) co_localsplusnames 里面的每個(gè)變量的種類

我們說(shuō)了,co_localsplusnames 里面包含了局部變量、cell 變量、free 變量的名稱,它們整體是作為一個(gè)元組存儲(chǔ)的。那么問(wèn)題來(lái)了,當(dāng)從 co_localsplusnames 里面獲取一個(gè)變量時(shí),解釋器怎么知道這個(gè)變量是局部變量,還是 cell 變量或者 free 變量呢?

所以便有了 co_localspluskinds 字段,它是一段字節(jié)序列,一個(gè)字節(jié)對(duì)應(yīng)一個(gè)變量。

// 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] 對(duì)應(yīng)的便是局部變量。這里可能有人好奇,CO_FAST_HIDDEN 表示的是啥?顧名思義,該宏對(duì)應(yīng)的是隱藏變量,所謂隱藏變量指的就是那些在當(dāng)前作用域中不可見的變量。

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

比如列表推導(dǎo)式里面的循環(huán)變量,它就是一個(gè)隱藏變量,生命周期只局限于列表解析式內(nèi)部,不會(huì)泄露到當(dāng)前的局部作用域中。但 Python2 是會(huì)泄露的,如果你還要維護(hù) Python2 老項(xiàng)目的話,那么這里要多加注意。

圖片圖片

以上就是 co_localspluskinds 字段的作用。

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

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


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

如果你無(wú)法使用 IDE,那么便可通過(guò)該字段查看函數(shù)定義在哪個(gè)文件中。

co_name:代碼塊的名字

def foo():
    pass

print(foo.__code__.co_name)  # foo

對(duì)于函數(shù)來(lái)說(shuō),代碼塊的名字就是函數(shù)名。

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"

如果是類的成員函數(shù),那么會(huì)將類名一起返回。

co_linetable:存儲(chǔ)指令和源代碼行號(hào)之間的對(duì)應(yīng)關(guān)系

PyCodeObject 是源代碼編譯之后的產(chǎn)物,雖然兩者的結(jié)構(gòu)千差萬(wàn)別,但體現(xiàn)出的信息是一致的。像源代碼具有行號(hào),那么編譯成 PyCodeObject 之后,行號(hào)信息也應(yīng)該要有專門的字段來(lái)維護(hù),否則報(bào)錯(cuò)時(shí)我們就無(wú)法快速定位到行號(hào)。

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

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

我們通過(guò) dis 模塊反編譯一下。

圖片圖片

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

關(guān)于反編譯的具體細(xì)節(jié)后續(xù)會(huì)說(shuō),總之一個(gè)字節(jié)碼指令就是一個(gè)八位整數(shù)。對(duì)于當(dāng)前函數(shù)來(lái)說(shuō),它的字節(jié)碼偏移量和行號(hào)的對(duì)應(yīng)關(guān)系如下:

圖片圖片

當(dāng)偏移量為 0 時(shí),證明還沒(méi)有進(jìn)入到函數(shù)體,那么源代碼行號(hào)便是 def 關(guān)鍵字所在的行號(hào)。然后偏移量增加 2、行號(hào)增加 1,接著偏移量增加 4、行號(hào)增加 1、最后偏移量增加 8、行號(hào)增加 6。

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

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

co_weakreflist:弱引用列表

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

以上就是 PyCodeObject 里面的字段的含義,至于剩下的幾個(gè)字段目前先跳過(guò),后續(xù)涉及到的時(shí)候再說(shuō)。

圖片圖片

小結(jié)

  • Python 解釋器 = Python 編譯器 + Python 虛擬機(jī)。
  • 編譯器先將 .py 源碼文件編譯成 PyCodeObject 對(duì)象,然后再交給虛擬機(jī)執(zhí)行。
  • PyCodeObject 對(duì)象可以認(rèn)為是源碼文件的另一種等價(jià)形式,但經(jīng)過(guò)編譯,虛擬機(jī)可以更快速地執(zhí)行。
  • 為了避免每次都要對(duì)源文件進(jìn)行編譯,因此編譯后的結(jié)果會(huì)序列化在 .pyc 文件中,如果源文件沒(méi)有做改動(dòng),那么下一次執(zhí)行時(shí)會(huì)直接從 .pyc 中讀取。
  • Python 的函數(shù)、類、模塊等,都具有各自的作用域,每個(gè)作用域?qū)?yīng)一個(gè)獨(dú)立的代碼塊,在編譯時(shí),Python 編譯器會(huì)為每個(gè)代碼塊都創(chuàng)建一個(gè) PyCodeObject 對(duì)象。

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

責(zé)任編輯:武曉燕 來(lái)源: 古明地覺(jué)的編程教室
相關(guān)推薦

2024-10-28 12:06:09

2009-09-08 18:02:20

CCNA用途

2024-05-22 08:02:30

2015-09-18 13:08:36

更新RedstoneWindows 10

2020-04-21 12:09:47

JVM消化字節(jié)碼

2024-05-07 09:24:12

Python源碼Java

2024-11-25 12:20:00

Hystrix微服務(wù)架構(gòu)

2020-08-10 15:48:01

Python輪子計(jì)算

2024-08-08 11:05:22

2023-04-17 14:21:19

5G無(wú)線技術(shù)

2022-02-24 23:37:19

區(qū)塊鏈錢包比特幣

2022-08-08 07:04:34

URLIPHTTP

2023-05-04 00:16:39

數(shù)字化轉(zhuǎn)型運(yùn)營(yíng)

2013-05-31 09:17:31

云計(jì)算云技術(shù)大數(shù)據(jù)

2024-07-30 14:01:51

Java字節(jié)碼JVM?

2024-12-03 09:34:35

觀察者模 式編程Javav

2023-11-07 08:00:00

Kubernetes

2018-03-22 14:47:13

容器開發(fā)人員筆記本

2022-08-26 16:32:08

云計(jì)算公有云私有云

2021-05-25 07:59:59

Linux運(yùn)維Linux系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 97国产精品| 天天操综合网站 | 中文字幕在线观看一区二区 | 中文字幕亚洲一区二区三区 | 成人av一区 | 欧美在线一区二区三区 | 91免费看片 | 欧美一级二级三级视频 | www.日本三级| 亚洲欧美在线一区 | 最新中文字幕在线播放 | 精品少妇一区二区三区在线播放 | 中文av网站 | 黄网站免费在线看 | 亚洲品质自拍视频网站 | 久久国产区| 精品久久一区 | 老司机精品福利视频 | 成人一区二区三区 | 精品免费国产一区二区三区四区介绍 | 久久一久久 | 国产精品毛片一区二区在线看 | 日韩一区二区在线播放 | 在线不卡一区 | 男女羞羞视频网站 | av大片在线观看 | 黄网站涩免费蜜桃网站 | 久久精品中文字幕 | 五月免费视频 | 亚洲最大成人综合 | av网站在线免费观看 | 99re在线免费视频 | 国产亚洲一区在线 | 国产中文| 成人久久久久久久久 | 九九热re| 性欧美精品一区二区三区在线播放 | 久久一区二区三区电影 | 国产精品一二区 | 成人性视频免费网站 | 性大毛片视频 |