Python 源文件編譯之后會(huì)得到什么,它的結(jié)構(gòu)是怎樣的?和字節(jié)碼又有什么聯(lián)系?
楔子
當(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)。