久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

《源碼探秘 CPython》44. 解析PyCodeObject對象

 古明地覺O_o 2022-12-08 發(fā)布于北京

心痛



獲取PyCodeObject對象



在上一篇文章中,,我們知道了py文件編譯之后會生成PyCodeObject對象,并且還會保存在pyc文件里,。那么我們在Python里面如何才能訪問到這個對象呢,?

首先PyCodeObject對象在Python里面的類型是<class 'code'>,,但是這個類Python沒有暴露給我們,因此code這個名字在Python里面只是一個沒有定義的變量罷了,。

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

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

我們可以通過函數(shù)的__code__屬性拿到底層對應(yīng)的PyCodeObject對象,,當(dāng)然也可以獲取里面的成員,,我們來演示一下。

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

def foo(a, b, c=3):    passprint(foo.__code__.co_argcount)  # 3
def bar(a, b, *args): passprint(bar.__code__.co_argcount) # 2
def func(a, b, *args, c): passprint(func.__code__.co_argcount) # 2

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

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

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

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

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

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

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

def foo(a, b, *, c):    name = "xxx"    age = 16    gender = "f"    c = 33
print(foo.__code__.co_nlocals) # 6

局部變量有 a,、b,、c、name,、age,、gender,所以我們看到在編譯之后,,函數(shù)的局部變量就已經(jīng)確定了,,因為它們是靜態(tài)存儲的。

co_stacksize:執(zhí)行該段代碼塊需要的??臻g

def foo(a, b, *, c):    name = "xxx"    age = 16    gender = "f"    c = 33
print(foo.__code__.co_stacksize) # 1

這個暫時不需要太關(guān)注,。

co_flags:參數(shù)類型標(biāo)識

如果一個函數(shù)的參數(shù)出現(xiàn)了 *args,,那么co_flags&0x04為真;如果一個函數(shù)的參數(shù)出現(xiàn)了 **kwargs,,那么co_flags&0x08為真,;

def foo1():    pass# 結(jié)果全部為假print(foo1.__code__.co_flags & 0x04)  # 0print(foo1.__code__.co_flags & 0x08)  # 0
def foo2(*args): pass# co_flags & 0x04 為真,因為出現(xiàn)了 *argsprint(foo2.__code__.co_flags & 0x04) # 4print(foo2.__code__.co_flags & 0x08) # 0
def foo3(*args, **kwargs): pass# 顯然 co_flags & 0x04 和 co_flags & 0x08 均為真print(foo3.__code__.co_flags & 0x04) # 4print(foo3.__code__.co_flags & 0x08) # 8

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

這些在詞法分析的時候就可以檢測出來,,編譯之后會體現(xiàn)在 co_flags 這個成員中,我們舉個栗子:

# 如果是生成器函數(shù)# 那么 co_flags & 0x20 為真def foo1():    yieldprint(foo1.__code__.co_flags & 0x20)  # 32
# 如果是協(xié)程函數(shù)# 那么 co_flags & 0x80 為真async def foo2(): passprint(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(): yieldprint(foo3.__code__.co_flags & 0x200) # 512# 顯然它不是生成器函數(shù),、也不是協(xié)程函數(shù)# 因此和 0x20,、0x80 按位與之后,結(jié)果都為假print(foo3.__code__.co_flags & 0x20) # 0print(foo3.__code__.co_flags & 0x80) # 0

以上就是 co_flags 的作用,。

co_firstlineno:代碼塊在對應(yīng)文件的起始行

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,那么會返回函數(shù)bar,,最終得到的就是bar的字節(jié)碼,,因此返回def bar():所在的行數(shù)。所以每個函數(shù)都有自己的作用域,,以及PyCodeObject對象,。

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

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

co_varnames:符號表,,一個元組,,保存在當(dāng)前作用域中的變量

c = 1
def foo(a, b): print(a, b, c) d = (list, int, str)print(foo.__code__.co_varnames) # ('a', 'b', 'd')

a,、b、d是位于當(dāng)前foo函數(shù)的作用域當(dāng)中的,,所以編譯階段便確定了局部變量是什么,。

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

x = 123
def foo(a, b): c = "abc" print(x) print(True, False, list, [1, 2, 3], {"a": 1}) return ">>>"
print( foo.__code__.co_consts)  # (None, 'abc', True, False, 1, 2, 3, 'a', '>>>')

co_consts里面出現(xiàn)的都是常量,,但[1, 2, 3]{"a": 1}卻沒有出現(xiàn),由此我們可以得出,,列表和字典絕不是在編譯階段構(gòu)建的,。編譯時,只是收集了里面的元素,,然后等到運行時再去動態(tài)構(gòu)建,。

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

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

co_freevars:內(nèi)層函數(shù)引用的外層函數(shù)的作用域中的變量

def f1():    a = 1    b = 2    def f2():        print(a)    return f2
# 這里拿到的是f2的字節(jié)碼print(f1().__code__.co_freevars) # ('a',)

函數(shù)f2引用了函數(shù)f1中的變量a,。

co_cellvars:外層函數(shù)的作用域中被內(nèi)層函數(shù)引用的變量,本質(zhì)上和co_freevars是一樣的

def f1():        a = 1    b = 2    def f2():        print(a)    return f2
# 但這里調(diào)用的是f1的字節(jié)碼print(f1.__code__.co_cellvars) # ('a',)

函數(shù)f1中的變量a被內(nèi)層函數(shù)f2引用了,。

co_filename:代碼塊所在的文件名

def foo():    pass    print(foo.__code__.co_filename)  # D:/satori/main.py

co_name:代碼塊的名字

def foo():    pass# 這里就是函數(shù)名print(foo.__code__.co_name)  # foo

co_code:字節(jié)碼

def foo(a, b, /, c, *, d, e):    f = 123    g = list()    g.extend([tuple, getattr, print])
print(foo.__code__.co_code)#b'd\x01}\x05t\x00\x83\x00}\x06|\x06\xa0\x01t\x02t\x03t\x04g\x03\xa1\x01\x01\x00d\x00S\x00'

這便是字節(jié)碼,,它只保存了要操作的指令,因此光有字節(jié)碼是肯定不夠的,,還需要其它的靜態(tài)信息,。顯然這些信息連同字節(jié)碼一樣,都位于PyCodeObject中,。

co_lnotab:字節(jié)碼指令與源代碼行號之間的對應(yīng)關(guān)系,,以PyByteObject的形式存在

def foo(a, b, /, c, *, d, e):    f = 123    g = list()    g.extend([tuple, getattr, print])    print(foo.__code__.co_lnotab)  # b'\x00\x01\x04\x01\x06\x01'

我們知道一行py代碼會對應(yīng)多條字節(jié)碼指令,但事實上,co_lnotab沒有直接記錄這些信息,,記錄的是增量值,。比如說:

那么co_lnotab就應(yīng)該是: 0 1 6 1 44 5,其中0和1很好理解,,就是co_code和.py文件的起始位置,。

而6和1表示字節(jié)碼的偏移量增加了6,.py文件的行號增加了1,;

而44和5表示字節(jié)碼的偏移量增加了44,,而.py文件的行號增加了5。

以上我們就分析了PyCodeObject里面的成員都代表什么含義,。


compile



我們上面通過函數(shù)的__code__屬性獲取了該函數(shù)的PyCodeObject對象,,但是還有沒有其他的方法呢?顯然是有的,,答案是通過內(nèi)置函數(shù)compile,,不過在介紹compile之前,先介紹一下eval和exec,。

eval:傳入一個字符串,然后把字符串里面的內(nèi)容拿出來,。

a = 1# 所以eval("a")就等價于aprint(eval("a"))  # 1
print(eval("1 + 1 + 1")) # 3

注意:eval是有返回值的,,返回值就是字符串里面內(nèi)容?;蛘哒feval是可以作為右值的,,比如a=eval("xxx")

所以eval里面一定是一個表達(dá)式,,表達(dá)式計算之后是一個具體的值,。絕不可以是語句,比如a=eval("b=3"),,這樣等價于a=(b=3),,顯然這會出現(xiàn)語法錯誤。

因此eval里面把字符串剝掉之后就是一個普通的值,,不可以出現(xiàn)諸如if,、def等語句。

try:    eval("xxx")except NameError as e:    print(e)  # name 'xxx' is not defined

此時等價于xxx,,但是xxx沒有定義,,所以報錯。

# 此時是合法的,,等價于 print('xxx')print(eval("'xxx'"))  # xxx

exec:傳入一個字符串,,把字符串里面的內(nèi)容當(dāng)成語句來執(zhí)行,這個是沒有返回值的,,或者說返回值是None,。

# 相當(dāng)于 a = 1exec("a = 1")  print(a)  # 1
statement = """a = 123if a == 123: print("a等于123")else: print("a不等于123")"""exec(statement) # a等于123

注意:a等于123并不是exec返回的,,而是把上面那坨字符串當(dāng)成普通代碼執(zhí)行的時候print出來的。這便是exec的作用,,將字符串當(dāng)成語句來執(zhí)行,。

那么它和eval的區(qū)別就顯而易見了,eval是要求字符串里面的內(nèi)容能夠當(dāng)成一個值,,返回值就是里面的值,。而exec則是直接執(zhí)行里面的內(nèi)容,返回值是None,。

print(eval("1 + 1"))  # 2print(exec("1 + 1"))  # None
# 相當(dāng)于 a = 2exec("a = 1 + 1")print(a) # 2
try:    # 相當(dāng)于a=2,,但很明顯,a=2是一個語句    # 它無法作為一個值,,因此放到eval里面就報錯了 eval("a = 1 + 1")except SyntaxError as e: print(e) # invalid syntax (<string>, line 1)

還是很好區(qū)分的,,但是eval和exec在生產(chǎn)中盡量要少用。另外,,eval 和 exec 還可以接收第二個參數(shù)和第三個參數(shù),,我們在介紹名字空間的時候再說。

compile:關(guān)鍵來了,,它執(zhí)行后返回的就是一個PyCodeObject對象,。

這個函數(shù)接收哪些參數(shù)呢?參數(shù)一:當(dāng)成代碼執(zhí)行的字符串,;參數(shù)二:可以為這些代碼起一個文件名,;參數(shù)三:執(zhí)行方式,支持三種,,分別是exec,、single、eval,。

  • exec:將源代碼當(dāng)做一個模塊來編譯,;

  • single:用于編譯一個單獨的Python語句(交互式下);

  • eval:用于編譯一個eval表達(dá)式,;

statement = "a, b = 1, 2"# 這里我們選擇 exec,,當(dāng)成一個模塊來編譯co = compile(statement, "古明地覺的 Python小屋", "exec")print(co.co_firstlineno)  # 1print(co.co_filename)  # 古明地覺的 Python小屋print(co.co_argcount)  # 0# 我們是a, b = 1, 2這種方式賦值# 所以(1, 2)會被當(dāng)成一個元組加載進(jìn)來# 從這里我們看到,元組是在編譯階段就已經(jīng)確定好了print(co.co_consts)  # ((1, 2), None)
statement = """a = 1b = 2"""co = compile(statement, "<file>", "exec")print(co.co_consts) # (1, 2, None)print(co.co_names) # ('a', 'b')

我們后面在分析PyCodeObject的時候,,會經(jīng)常使用compile的方式,。

然后 compile 還可以接收一個 flags 參數(shù),也就是第四個參數(shù),,如果指定為 1024,,那么得到的就不再是PyCodeObject對象了,而是一個_ast.Module 對象。

print(    compile("a = 1", "<file>", "exec").__class__)  # <class 'code'>
print( compile("a = 1", "<file>", "exec", flags=1024).__class__) # <class '_ast.Module'>

_ast 是用 C 實現(xiàn)的模塊,,內(nèi)嵌在解釋器里面,,用于幫助我們更好地理解Python的抽象語法樹。當(dāng)然,,構(gòu)建抽象語法樹的話,我們更習(xí)慣使用標(biāo)準(zhǔn)庫中的 ast 模塊,,它里面了導(dǎo)入了 _ast,。

那么問題來了,這個 _ast.Module 對象能夠干什么呢,?別著急,,我們后續(xù)在介紹棧幀的時候說,。不過由于抽象語法樹比較底層,對我們理解Python沒有什么實質(zhì)性的幫助,,因此知道 compile 的前三個參數(shù)的用法即可,。


字節(jié)碼與反編譯



關(guān)于Python的字節(jié)碼,,是后面剖析虛擬機的重點,,現(xiàn)在先來看一下。我們知道Python執(zhí)行源代碼之前會先編譯得到PyCodeObject對象,,里面的co_code指向了字節(jié)碼序列,。

Python虛擬機會根據(jù)這些字節(jié)碼序列來進(jìn)行一系列的操作(當(dāng)然也依賴其它的靜態(tài)信息),從而完成對程序的執(zhí)行,。

每個操作都對應(yīng)一個操作指令,、也叫操作碼,總共有120多種,定義在Include/opcode.h中,。

#define POP_TOP                   1#define ROT_TWO                   2#define ROT_THREE                 3#define DUP_TOP                   4#define DUP_TOP_TWO               5#define NOP                       9#define UNARY_POSITIVE           10#define UNARY_NEGATIVE           11#define UNARY_NOT                12#define UNARY_INVERT             15#define BINARY_MATRIX_MULTIPLY   16#define INPLACE_MATRIX_MULTIPLY  17#define BINARY_POWER             19#define BINARY_MULTIPLY          20#define BINARY_MODULO            22#define BINARY_ADD               23#define BINARY_SUBTRACT          24#define BINARY_SUBSCR            25#define BINARY_FLOOR_DIVIDE      26#define BINARY_TRUE_DIVIDE       27#define INPLACE_FLOOR_DIVIDE     28......

操作指令只是一個整數(shù),,然后我們可以通過反編譯的方式查看每行Python代碼都對應(yīng)哪些操作指令:

# Python中的dis模塊專門負(fù)責(zé)干這件事情import dis
def foo(a, b): c = a + b return c
# 里面接收一個字節(jié)碼# 當(dāng)然函數(shù)也是可以的,會自動獲取co_codedis.dis(foo)""" 5 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 BINARY_ADD 6 STORE_FAST 2 (c)
6 8 LOAD_FAST 2 (c) 10 RETURN_VALUE"""

字節(jié)碼反編譯后的結(jié)果多么像匯編語言,,其中第一列是源代碼行號,,第二列是字節(jié)碼偏移量,第三列是操作指令,。關(guān)于反編譯的內(nèi)容,,我們會在剖析函數(shù)的時候,深入介紹,。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多