PyCodeObject 拾遺
內(nèi)置函數(shù) compile
之前通過函數(shù)的 __code__ 屬性獲取了該函數(shù)的 PyCodeObject 對(duì)象,但是還有沒有其它的方法呢?顯然是有的,答案是通過內(nèi)置函數(shù) compile,不過在介紹 compile 之前,先介紹一下 eval 和 exec。
eval:傳入一個(gè)字符串,然后把字符串里面的內(nèi)容當(dāng)做表達(dá)式
a = 1
# 所以 eval("a") 就等價(jià)于 a
print(eval("a")) # 1
print(eval("1 + 1 + 1")) # 3
注意:eval 是有返回值的,返回值就是字符串里面的內(nèi)容。所以 eval 接收的字符串里面一定是一個(gè)表達(dá)式,表達(dá)式計(jì)算之后是一個(gè)具體的值,比如 a = eval("1 + 2"),等價(jià)于 a = 3。
但如果是語(yǔ)句的話,比如 a = eval("b = 3"),這樣等價(jià)于 a = (b = 3),顯然這會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤。因此 eval 函數(shù)把字符串兩邊的引號(hào)剝掉之后,得到的一定是一個(gè)普通的值。
try:
print(eval("xxx"))
except NameError as e:
print(e) # name 'xxx' is not defined
此時(shí)等價(jià)于 print(xxx),但是 xxx 沒有定義,所以報(bào)錯(cuò)。
# 此時(shí)是合法的,等價(jià)于 print('xxx')
print(eval("'xxx'")) # xxx
以上就是 eval 函數(shù),使用起來還是很方便的。
exec:傳入一個(gè)字符串,把字符串里面的內(nèi)容當(dāng)成語(yǔ)句來執(zhí)行,這個(gè)是沒有返回值的,或者說返回值是 None。
# 相當(dāng)于 a = 1
exec("a = 1")
print(a) # 1
statement = """
a = 123
if a == 123:
print("a 等于 123")
else:
print("a 不等于 123")
"""
exec(statement) # a 等于 123
注意:a 等于 123 并不是 exec 返回的,而是把上面那坨字符串當(dāng)成普通代碼執(zhí)行的時(shí)候 print 出來的。這便是 exec 的作用,將字符串當(dāng)成語(yǔ)句來執(zhí)行。
所以使用 exec 可以非常方便地創(chuàng)建多個(gè)變量。
import random
for i in range(1, 5):
exec(f"a{i} = {random.randint(1, 100)}")
print(a1) # 72
print(a2) # 21
print(a3) # 38
print(a4) # 32
那么 exec 和 eval 的區(qū)別就顯而易見了,eval 是要求字符串里面的內(nèi)容能夠當(dāng)成一個(gè)值,并且該值就是 eval 函數(shù)的返回值。而 exec 則是直接執(zhí)行里面的內(nèi)容,返回值是 None。
print(eval("1 + 1")) # 2
print(exec("1 + 1")) # None
# 相當(dāng)于 a = 2
exec("a = 1 + 1")
print(a) # 2
try:
# 相當(dāng)于 a = 2,但很明顯 a = 2 是一個(gè)語(yǔ)句
# 它無法作為一個(gè)值,因此放到 eval 里面就報(bào)錯(cuò)了
eval("a = 1 + 1")
except SyntaxError as e:
print(e) # invalid syntax (<string>, line 1)
還是很好區(qū)分的,但是 eval 和 exec 在生產(chǎn)中盡量要少用。另外,eval 和 exec 還可以接收第二個(gè)參數(shù)和第三個(gè)參數(shù),我們?cè)诮榻B名字空間的時(shí)候再說。
compile:關(guān)鍵來了,它執(zhí)行后返回的就是一個(gè) PyCodeObject 對(duì)象。
這個(gè)函數(shù)接收哪些參數(shù)呢?
- 參數(shù)一:當(dāng)成代碼執(zhí)行的字符串
- 參數(shù)二:可以為這些代碼起一個(gè)文件名
- 參數(shù)三:執(zhí)行方式,支持三種,分別是 exec、single、eval
我們演示一下。
# exec:將源代碼當(dāng)做一個(gè)模塊來編譯
# single:用于編譯一個(gè)單獨(dú)的 Python 語(yǔ)句(交互式)
# eval:用于編譯一個(gè) eval 表達(dá)式
statement = "a, b = 1, 2"
# 這里我們選擇 exec,當(dāng)成一個(gè)模塊來編譯
co = compile(statement, "古明地覺的編程教室", "exec")
print(co.co_firstlineno) # 1
print(co.co_filename) # 古明地覺的編程教室
print(co.co_argcount) # 0
# 我們是以 a, b = 1, 2 這種方式賦值
# 所以 (1, 2) 會(huì)被當(dāng)成一個(gè)元組加載進(jìn)來
# 從這里我們看到,元組是在編譯階段就已經(jīng)確定好了
print(co.co_consts) # ((1, 2), None)
statement = """
a = 1
b = 2
"""
co = compile(statement, "<file>", "exec")
print(co.co_consts) # (1, 2, None)
print(co.co_names) # ('a', 'b')
我們后面在分析 PyCodeObject 的時(shí)候,會(huì)經(jīng)常使用 compile 函數(shù)。
然后 compile 還可以接收一個(gè) flags 參數(shù),也就是第四個(gè)參數(shù),它的默認(rèn)值為 0,表示按照標(biāo)準(zhǔn)模式進(jìn)行編譯,就是之前說的那幾步。
- 對(duì)文本形式的源代碼進(jìn)行分詞,將其切分成一個(gè)個(gè)的 Token;
- 對(duì) Token 進(jìn)行語(yǔ)法解析,生成抽象語(yǔ)法樹(AST);
- 將 AST 編譯成 PyCodeObject 對(duì)象,簡(jiǎn)稱 code 對(duì)象或者代碼對(duì)象;
但如果將 flags 指定為 1024,那么 compile 函數(shù)在生成 AST 之后會(huì)直接停止,然后返回一個(gè) ast.Module 對(duì)象。
print(
compile("a = 1", "<file>", "exec").__class__
) # <class 'code'>
print(
compile("a = 1", "<file>", "exec", flags=1024).__class__
) # <class 'ast.Module'>
ast 模塊是和 Python 的抽象語(yǔ)法樹相關(guān)的,那么問題來了,這個(gè) ast.Module 對(duì)象能夠干什么呢?別著急,我們后續(xù)在介紹棧幀的時(shí)候說。不過由于抽象語(yǔ)法樹比較底層,因此知道 compile 的前三個(gè)參數(shù)的用法即可。
字節(jié)碼與反編譯
關(guān)于 Python 的字節(jié)碼,是后面剖析虛擬機(jī)的重點(diǎn),現(xiàn)在先來看一下。我們知道執(zhí)行源代碼之前會(huì)先編譯得到 PyCodeObject 對(duì)象,里面的 co_code 字段指向了字節(jié)碼序列,或者說字節(jié)碼指令集。
虛擬機(jī)會(huì)根據(jù)這些指令集來進(jìn)行一系列的操作(當(dāng)然也依賴其它的靜態(tài)信息),從而完成對(duì)程序的執(zhí)行。關(guān)于指令,解釋器定義了 200 多種,我們大致看一下。
// Include/opcode.h
#define CACHE 0
#define POP_TOP 1
#define PUSH_NULL 2
#define INTERPRETER_EXIT 3
#define END_FOR 4
#define END_SEND 5
#define NOP 9
#define UNARY_NEGATIVE 11
#define UNARY_NOT 12
#define UNARY_INVERT 15
#define RESERVED 17
#define BINARY_SUBSCR 25
#define BINARY_SLICE 26
#define STORE_SLICE 27
#define GET_LEN 30
#define MATCH_MAPPING 31
#define MATCH_SEQUENCE 32
#define MATCH_KEYS 33
#define PUSH_EXC_INFO 35
#define CHECK_EXC_MATCH 36
#define CHECK_EG_MATCH 37
#define WITH_EXCEPT_START 49
#define GET_AITER 50
#define GET_ANEXT 51
#define BEFORE_ASYNC_WITH 52
#define BEFORE_WITH 53
#define END_ASYNC_FOR 54
#define CLEANUP_THROW 55
#define STORE_SUBSCR 60
#define DELETE_SUBSCR 61
#define GET_ITER 68
#define GET_YIELD_FROM_ITER 69
#define LOAD_BUILD_CLASS 71
#define LOAD_ASSERTION_ERROR 74
#define RETURN_GENERATOR 75
#define RETURN_VALUE 83
// ...
// ...
所謂字節(jié)碼指令其實(shí)就是個(gè)整數(shù),多個(gè)指令組合在一起便是字節(jié)碼指令集(字節(jié)碼序列),它是一個(gè) bytes 對(duì)象。當(dāng)然啦,指令集里面不全是指令,索引(偏移量)為偶數(shù)的字節(jié)表示指令,索引為奇數(shù)的字節(jié)表示指令參數(shù),后續(xù)會(huì)細(xì)說。
然后我們可以通過反編譯的方式查看每行 Python 代碼都對(duì)應(yīng)哪些操作指令。
# Python 的 dis 模塊專門負(fù)責(zé)干這件事情
import dis
def foo(a, b):
c = a + b
return c
# 里面接收 PyCodeObject 對(duì)象
# 當(dāng)然函數(shù)也是可以的,會(huì)自動(dòng)獲取 co_code
dis.dis(foo)
"""
1 0 RESUME 0
2 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 STORE_FAST 2 (c)
3 12 LOAD_FAST 2 (c)
14 RETURN_VALUE
"""
字節(jié)碼反編譯后的結(jié)果多么像匯編語(yǔ)言,其中第一列是源代碼行號(hào),第二列是字節(jié)碼偏移量,第三列是字節(jié)碼指令(也叫操作碼),第四列是指令參數(shù)(也叫操作數(shù))。Python 的字節(jié)碼指令都是成對(duì)出現(xiàn)的,每個(gè)指令會(huì)帶有一個(gè)指令參數(shù)。
查看字節(jié)碼也可以使用 opcode 模塊:
from opcode import opmap
opmap = {v: k for k, v in opmap.items()}
def foo(a, b):
c = a + b
return c
code = foo.__code__.co_code
for i in range(0, len(code), 2):
print("操作碼: {:<12} 操作數(shù): {}".format(
opmap[code[i]], code[i+1]
))
"""
操作碼: RESUME 操作數(shù): 0
操作碼: LOAD_FAST 操作數(shù): 0
操作碼: LOAD_FAST 操作數(shù): 1
操作碼: BINARY_OP 操作數(shù): 0
操作碼: CACHE 操作數(shù): 0
操作碼: STORE_FAST 操作數(shù): 2
操作碼: LOAD_FAST 操作數(shù): 2
操作碼: RETURN_VALUE 操作數(shù): 0
"""
總之字節(jié)碼就是一段字節(jié)序列,轉(zhuǎn)成列表之后就是一堆數(shù)字。偶數(shù)位置表示指令本身,而每個(gè)指令后面都會(huì)跟一個(gè)指令參數(shù),也就是奇數(shù)位置表示指令參數(shù)。
所以指令本質(zhì)上只是一個(gè)整數(shù):
圖片
虛擬機(jī)會(huì)根據(jù)不同的指令執(zhí)行不同的邏輯,說白了 Python 虛擬機(jī)執(zhí)行字節(jié)碼的邏輯就是把自己想象成一顆 CPU,并內(nèi)置了一個(gè)巨型的 switch case 語(yǔ)句,其中每個(gè)指令都對(duì)應(yīng)一個(gè) case 分支。
然后遍歷整條字節(jié)碼,拿到每一個(gè)指令和指令參數(shù)。接著對(duì)指令進(jìn)行判斷,不同的指令進(jìn)入不同的 case 分支,執(zhí)行不同的處理邏輯,直到字節(jié)碼全部執(zhí)行完畢或者程序出錯(cuò)。
關(guān)于執(zhí)行字節(jié)碼的具體流程,等介紹棧幀的時(shí)候細(xì)說。