最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

令人拍案叫絕的python字節(jié)碼設計

2023-04-03 08:56 作者:程序員-王堅  | 我要投稿

python 字節(jié)碼設計

一條 python 字節(jié)碼主要有兩部分組成,一部分是操作碼,一部分是這個操作碼的參數(shù),在 cpython 當中只有部分字節(jié)碼有參數(shù),如果對應的字節(jié)碼沒有參數(shù),那么 oparg 的值就等于 0 ,在 cpython 當中 opcode < 90 的指令是沒有參數(shù)的。

opcode 和 oparg 各占一個字節(jié),cpython 虛擬機使用小端方式保存字節(jié)碼。

我們使用下面的代碼片段先了解一下字節(jié)碼的設計:

import disdef add(a, b):return a + bif __name__ == '__main__':print(add.__code__.co_code)print("bytecode: ", list(bytearray(add.__code__.co_code)))dis.dis(add)

上面的代碼在 python3.9 的輸出如下所示:

b'|\x00|\x01\x17\x00S\x00'bytecode: ?[124, 0, 124, 1, 23, 0, 83, 0]5 ? ? ? ? ? 0 LOAD_FAST ? ? ? ? ? ? ? ?0 (a)2 LOAD_FAST ? ? ? ? ? ? ? ?1 (b)4 BINARY_ADD6 RETURN_VALUE

首先 需要了解的是 add.__code__.co_code 是函數(shù) add 的字節(jié)碼,是一個字節(jié)序列,list(bytearray(add.__code__.co_code))?是將和這個序列一個字節(jié)一個字節(jié)進行分開,并且將其變成 10 進制形式。根據(jù)前面我們談到的每一條指令——字節(jié)碼占用 2 個字節(jié),因此上面的字節(jié)碼有四條指令:

操作碼和對應的操作指令在文末有詳細的對應表。在上面的代碼當中主要使用到了三個字節(jié)碼指令分別是 124,23 和 83 ,他們對應的操作指令分別為 LOAD_FAST,BINARY_ADD,RETURN_VALUE。他們的含義如下:

  • LOAD_FAST:將 varnames[var_num] 壓入棧頂。

  • BINARY_ADD:從棧中彈出兩個對象并且將它們相加的結(jié)果壓入棧頂。

  • RETURN_VALUE:彈出棧頂?shù)脑兀瑢⑵渥鳛楹瘮?shù)的返回值。

首先我們需要知道的是 BINARY_ADD 和 RETURN_VALUE,這兩個操作指令是沒有參數(shù)的,因此在這兩個操作碼之后的參數(shù)都是 0 。

但是 LOAD_FAST 是有參數(shù)的,在上面我們已經(jīng)知道 LOAD_FAST 是將 co-varnames[var_num] 壓入棧,var_num 就是指令 LOAD_FAST 的參數(shù)。在上面的代碼當中一共有兩條 LOAD_FAST 指令,分別是將 a 和 b 壓入到棧中,他們在 varnames 當中的下標分別是 0 和 1,因此他們的操作數(shù)就是 0 和 1 。

字節(jié)碼擴展參數(shù)

在上面我們談到的 python 字節(jié)碼操作數(shù)和操作碼各占一個字節(jié),但是如果 varnames 或者常量表的數(shù)據(jù)的個數(shù)大于 1 個字節(jié)的表示范圍的話那么改如何處理呢?

為了解決這個問題,cpython 為字節(jié)碼設計的擴展參數(shù),比如說我們要加載常量表當中的下標為 66113 的對象,那么對應的字節(jié)碼如下:

[144, 1, 144, 2, 100, 65]

其中 144 表示 EXTENDED_ARG,他本質(zhì)上不是一個 python 虛擬機需要執(zhí)行的字節(jié)碼,這個字段設計出來主要是為了用與計算擴展參數(shù)的。

100 對應的操作指令是 LOAD_CONST ,其操作碼是 65,但是上面的指令并不會加載常量表當中下標為 65 對象,而是會加載下標為 66113 的對象,原因就是因為 EXTENDED_ARG 。

現(xiàn)在來模擬一下上面的分析過程:

  • 先讀取一條字節(jié)碼指令,操作碼等于 144 ,說明是擴展參數(shù),那么此時的參數(shù) arg 就等于 (1 x (1 << 8)) = 256 。

  • 讀取第二條字節(jié)碼指令,操作碼等于 144 ,說明是擴展參數(shù),因為前面 arg 已經(jīng)存在切不等于 0 了,那么此時 arg 的計算方式已經(jīng)發(fā)生了改變,arg = arg << 8 + 2 << 8 ,也就是說原來的 arg 乘以 256 再加上新的操作數(shù)乘以 256 ,此時 arg = 66048 。

  • 讀取第三條字節(jié)碼指令,操作碼等于 100,此時是 LOAD_CONST 這條指令,那么此時的操作碼等于 arg += 65,因為操作碼不是 EXTENDED_ARG 因此操作數(shù)不需要在乘以 256 了。

上面的計算過程用程序代碼表示如下,下面的代碼當中 code 就是真正的字節(jié)序列 HAVE_ARGUMENT = 90 。

def _unpack_opargs(code):extended_arg = 0for i in range(0, len(code), 2):op = code[i]if op >= HAVE_ARGUMENT:arg = code[i+1] | extended_argextended_arg = (arg << 8) if op == EXTENDED_ARG else 0else:arg = Noneyield (i, op, arg)

我們可以使用代碼來驗證我們前面的分析:

import disdef num_to_byte(n):return n.to_bytes(1, "little")def nums_to_bytes(data):ans = b"".join([num_to_byte(n) for n in data])return ansif __name__ == '__main__':# extended_arg extended_num opcode oparg for python_version > 3.5bytecode = nums_to_bytes([144, 1, 144, 2, 100, 65])print(bytecode)dis.dis(bytecode)

上面的代碼輸出結(jié)果如下所示:

b'\x90\x01\x90\x02dA'0 EXTENDED_ARG ? ? ? ? ? ? 12 EXTENDED_ARG ? ? ? ? ? 2584 LOAD_CONST ? ? ? ? ? 66113 (66113)

根據(jù)上面程序的輸出結(jié)果可以看到我們的分析結(jié)果是正確的。

源代碼字節(jié)碼映射表

在本小節(jié)主要分析一個 code object 對象當中的 co_lnotab 字段,通過分析一個具體的字段來學習這個字段的設計。

import disdef add(a, b):a += 1b += 2return a + bif __name__ == '__main__':dis.dis(add.__code__)print(f"{list(bytearray(add.__code__.co_lnotab)) = }")print(f"{add.__code__.co_firstlineno = }")

首先 dis 的輸出第一列是字節(jié)碼對應的源代碼的行號,第二列是字節(jié)碼在字節(jié)序列當中的位移。

上面的代碼輸出結(jié)果如下所示:

源代碼的行號 ?字節(jié)碼的位移6 ? ? ? ? ? 0 LOAD_FAST ? ? ? ? ? ? ? ?0 (a)2 LOAD_CONST ? ? ? ? ? ? ? 1 (1)4 INPLACE_ADD6 STORE_FAST ? ? ? ? ? ? ? 0 (a)7 ? ? ? ? ? 8 LOAD_FAST ? ? ? ? ? ? ? ?1 (b)10 LOAD_CONST ? ? ? ? ? ? ? 2 (2)12 INPLACE_ADD14 STORE_FAST ? ? ? ? ? ? ? 1 (b)8 ? ? ? ? ?16 LOAD_FAST ? ? ? ? ? ? ? ?0 (a)18 LOAD_FAST ? ? ? ? ? ? ? ?1 (b)20 BINARY_ADD22 RETURN_VALUElist(bytearray(add.__code__.co_lnotab)) = [0, 1, 8, 1, 8, 1]add.__code__.co_firstlineno = 5

從上面代碼的輸出結(jié)果可以看出字節(jié)碼一共分成三段,每段表示一行代碼的字節(jié)碼。現(xiàn)在我們來分析一下 co_lnotab 這個字段,這個字段其實也是兩個字節(jié)為一段的。比如上面的 [0, 1, 8, 1, 8, 1] 就可以分成三段 [0, 1], [8, 1], [8, 1] 。這其中的含義分別為:

  • 第一個數(shù)字表示距離上一行代碼的字節(jié)碼數(shù)目。

  • 第二個數(shù)字表示距離上一行有效代碼的行數(shù)。

現(xiàn)在我們來模擬上面代碼的字節(jié)碼的位移和源代碼行數(shù)之間的關(guān)系:

  • [0, 1],說明這行代碼離上一行代碼的字節(jié)位移是 0 ,因此我們可以看到使用 dis 輸出的字節(jié)碼 LOAD_FAST ,前面的數(shù)字是 0,距離上一行代碼的行數(shù)等于 1 ,代碼的第一行的行號等于 5,因此 LOAD_FAST 對應的行號等于 5 + 1 = 6 。

  • [8, 1],說明這行代碼距離上一行代碼的字節(jié)位移為 8 個字節(jié),因此第二塊的 LOAD_FAST 前面是 8 ,距離上一行代碼的行數(shù)等于 1,因此這個字節(jié)碼對應的源代碼的行號等于 6 + 1 = 7。

  • [8, 1],同理可以知道這塊字節(jié)碼對應源代碼的行號是 8 。

現(xiàn)在有一個問題是當兩行代碼之間相距的行數(shù)超過 一個字節(jié)的表示范圍怎么辦?在 python3.5 以后如果行數(shù)差距大于 127,那么就使用 (0, 行數(shù)) 對下一個組合進行表示,(0,?x1), (0,x2) ... ,直到?x1+...+xn?= 行數(shù)。

在后面的程序當中我們會使用 compile 這個 python 內(nèi)嵌函數(shù)。當你使用Python編寫代碼時,可以使用compile()函數(shù)將Python代碼編譯成字節(jié)代碼對象。這個字節(jié)碼對象可以被傳遞給Python的解釋器或虛擬機,以執(zhí)行代碼。

compile()函數(shù)接受三個參數(shù):

  • source: 要編譯的Python代碼,可以是字符串,字節(jié)碼或AST對象。

  • filename: 代碼來源的文件名(如果有),通常為字符串。

  • mode: 編譯代碼的模式??梢允?'exec'、'eval' 或 'single' 中的一個。'exec' 模式用于編譯多行代碼,'eval' 用于編譯單個表達式,'single' 用于編譯單行代碼。

import discode = """x=1y=2""" \+ "\n" * 500 + \"""z=x+y"""code = compile(code, '<string>', 'exec')print(list(bytearray(code.co_lnotab)))print(code.co_firstlineno)dis.dis(code)

上面的代碼輸出結(jié)果如下所示:

[0, 1, 4, 1, 4, 127, 0, 127, 0, 127, 0, 121]12 ? ? ? ? ? 0 LOAD_CONST ? ? ? ? ? ? ? 0 (1)2 STORE_NAME ? ? ? ? ? ? ? 0 (x)3 ? ? ? ? ? 4 LOAD_CONST ? ? ? ? ? ? ? 1 (2)6 STORE_NAME ? ? ? ? ? ? ? 1 (y)505 ? ? ? ? ? 8 LOAD_NAME ? ? ? ? ? ? ? ?0 (x)10 LOAD_NAME ? ? ? ? ? ? ? ?1 (y)12 BINARY_ADD14 STORE_NAME ? ? ? ? ? ? ? 2 (z)16 LOAD_CONST ? ? ? ? ? ? ? 2 (None)18 RETURN_VALUE

根據(jù)我們前面的分析因為第三行和第二行之間的差距大于 127 ,因此后面的多個組合都是用于表示行數(shù)的。

505 = 3(前面已經(jīng)有三行了) + (127 + 127 + 127 + 121)(這個是第二行和第三行之間的差距,這個值為 502,中間有 500 個換行但是因為字符串相加的原因還增加了兩個換行,因此一共是 502 個換行)。

具體的算法用代碼表示如下所示,下面的參數(shù)就是我們傳遞給 dis 模塊的 code,也就是一個 code object 對象。

def findlinestarts(code):"""Find the offsets in a byte code which are start of lines in the source. ? ?Generate pairs (offset, lineno) as described in Python/compile.c. ? ?"""byte_increments = code.co_lnotab[0::2]line_increments = code.co_lnotab[1::2]bytecode_len = len(code.co_code)lastlineno = Nonelineno = code.co_firstlinenoaddr = 0for byte_incr, line_incr in zip(byte_increments, line_increments):if byte_incr:if lineno != lastlineno:yield (addr, lineno)lastlineno = linenoaddr += byte_incrif addr >= bytecode_len:# The rest of the lnotab byte offsets are past the end of# the bytecode, so the lines were optimized away.returnif line_incr >= 0x80:# line_increments is an array of 8-bit signed integersline_incr -= 0x100lineno += line_incrif lineno != lastlineno:yield (addr, lineno)


令人拍案叫絕的python字節(jié)碼設計的評論 (共 條)

分享到微博請遵守國家法律
定南县| 桦川县| 定南县| 嵊泗县| 赣州市| 手游| 青海省| 成武县| 大渡口区| 巴中市| 梧州市| 德州市| 遵化市| 嘉峪关市| 石渠县| 和政县| 招远市| 昆明市| 邵东县| 通榆县| 奇台县| 蕉岭县| 象山县| 临猗县| 扶风县| 上高县| 兴义市| 岱山县| 丽江市| 内黄县| 秀山| 江安县| 洞头县| 东城区| 杭锦旗| 六盘水市| 凌源市| 通山县| 共和县| 梁山县| 松潘县|