自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

PyCodeObject 拾遺

開發(fā) 前端
虛擬機會根據(jù)不同的指令執(zhí)行不同的邏輯,說白了 Python 虛擬機執(zhí)行字節(jié)碼的邏輯就是把自己想象成一顆 CPU,并內(nèi)置了一個巨型的 switch case 語句,其中每個指令都對應一個 case 分支。?

內(nèi)置函數(shù) compile

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

eval:傳入一個字符串,然后把字符串里面的內(nèi)容當做表達式

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

注意:eval 是有返回值的,返回值就是字符串里面的內(nèi)容。所以 eval 接收的字符串里面一定是一個表達式,表達式計算之后是一個具體的值,比如 a = eval("1 + 2"),等價于 a = 3。

但如果是語句的話,比如 a = eval("b = 3"),這樣等價于 a = (b = 3),顯然這會出現(xiàn)語法錯誤。因此 eval 函數(shù)把字符串兩邊的引號剝掉之后,得到的一定是一個普通的值。

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

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

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

以上就是 eval 函數(shù),使用起來還是很方便的。

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

# 相當于 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 返回的,而是把上面那坨字符串當成普通代碼執(zhí)行的時候 print 出來的。這便是 exec 的作用,將字符串當成語句來執(zhí)行。

所以使用 exec 可以非常方便地創(chuàng)建多個變量。

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)容能夠當成一個值,并且該值就是 eval 函數(shù)的返回值。而 exec 則是直接執(zhí)行里面的內(nèi)容,返回值是 None。

print(eval("1 + 1"))  # 2
print(exec("1 + 1"))  # None

# 相當于 a = 2
exec("a = 1 + 1")
print(a)  # 2

try:
    # 相當于 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:關鍵來了,它執(zhí)行后返回的就是一個 PyCodeObject 對象。

這個函數(shù)接收哪些參數(shù)呢?

  • 參數(shù)一:當成代碼執(zhí)行的字符串
  • 參數(shù)二:可以為這些代碼起一個文件名
  • 參數(shù)三:執(zhí)行方式,支持三種,分別是 exec、single、eval

我們演示一下。

# exec:將源代碼當做一個模塊來編譯
# single:用于編譯一個單獨的 Python 語句(交互式)
# eval:用于編譯一個 eval 表達式
statement = "a, b = 1, 2"
# 這里我們選擇 exec,當成一個模塊來編譯
co = compile(statement, "古明地覺的編程教室", "exec")

print(co.co_firstlineno)  # 1
print(co.co_filename)  # 古明地覺的編程教室
print(co.co_argcount)  # 0
# 我們是以 a, b = 1, 2 這種方式賦值
# 所以 (1, 2) 會被當成一個元組加載進來
# 從這里我們看到,元組是在編譯階段就已經(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 的時候,會經(jīng)常使用 compile 函數(shù)。

然后 compile 還可以接收一個 flags 參數(shù),也就是第四個參數(shù),它的默認值為 0,表示按照標準模式進行編譯,就是之前說的那幾步。

  • 對文本形式的源代碼進行分詞,將其切分成一個個的 Token;
  • 對 Token 進行語法解析,生成抽象語法樹(AST);
  • 將 AST 編譯成 PyCodeObject 對象,簡稱 code 對象或者代碼對象;

但如果將 flags 指定為 1024,那么 compile 函數(shù)在生成 AST 之后會直接停止,然后返回一個 ast.Module 對象。

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

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

ast 模塊是和 Python 的抽象語法樹相關的,那么問題來了,這個 ast.Module 對象能夠干什么呢?別著急,我們后續(xù)在介紹棧幀的時候說。不過由于抽象語法樹比較底層,因此知道 compile 的前三個參數(shù)的用法即可。

字節(jié)碼與反編譯

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

虛擬機會根據(jù)這些指令集來進行一系列的操作(當然也依賴其它的靜態(tài)信息),從而完成對程序的執(zhí)行。關于指令,解釋器定義了 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ù),多個指令組合在一起便是字節(jié)碼指令集(字節(jié)碼序列),它是一個 bytes 對象。當然啦,指令集里面不全是指令,索引(偏移量)為偶數(shù)的字節(jié)表示指令,索引為奇數(shù)的字節(jié)表示指令參數(shù),后續(xù)會細說。

然后我們可以通過反編譯的方式查看每行 Python 代碼都對應哪些操作指令。

# Python 的 dis 模塊專門負責干這件事情
import dis

def foo(a, b):
    c = a + b
    return c

# 里面接收 PyCodeObject 對象
# 當然函數(shù)也是可以的,會自動獲取 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é)果多么像匯編語言,其中第一列是源代碼行號,第二列是字節(jié)碼偏移量,第三列是字節(jié)碼指令(也叫操作碼),第四列是指令參數(shù)(也叫操作數(shù))。Python 的字節(jié)碼指令都是成對出現(xiàn)的,每個指令會帶有一個指令參數(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ù)位置表示指令本身,而每個指令后面都會跟一個指令參數(shù),也就是奇數(shù)位置表示指令參數(shù)。

所以指令本質(zhì)上只是一個整數(shù):

圖片圖片

虛擬機會根據(jù)不同的指令執(zhí)行不同的邏輯,說白了 Python 虛擬機執(zhí)行字節(jié)碼的邏輯就是把自己想象成一顆 CPU,并內(nèi)置了一個巨型的 switch case 語句,其中每個指令都對應一個 case 分支。

然后遍歷整條字節(jié)碼,拿到每一個指令和指令參數(shù)。接著對指令進行判斷,不同的指令進入不同的 case 分支,執(zhí)行不同的處理邏輯,直到字節(jié)碼全部執(zhí)行完畢或者程序出錯。

關于執(zhí)行字節(jié)碼的具體流程,等介紹棧幀的時候細說。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2022-10-12 23:39:46

Java接口屬性

2022-10-11 09:33:04

Java異常Exception

2018-11-30 15:17:38

CPUCache緩存行

2010-03-30 08:36:26

Java框架StrutsSpring

2021-03-19 16:05:33

CSS CSS 屬性CSS 基礎

2021-12-14 07:40:07

C# 異步流結(jié)合體

2021-06-25 10:18:08

JavaScript Array.map 巧技拾遺

2021-11-23 20:41:05

對象軟件設計

2010-10-11 13:03:00

Windows Pho

2024-09-30 16:08:43

Python虛擬機棧幀

2014-01-09 11:28:21

Windows 9

2009-07-07 08:33:51

微軟Windows 7操作系統(tǒng)

2016-07-29 10:37:00

互聯(lián)網(wǎng)httphtml

2022-04-18 10:13:54

元宇宙區(qū)塊鏈百悟科技

2015-03-11 16:52:55

華為華為渠道

2011-12-26 15:59:49

4GLTE無線基礎

2016-11-04 14:44:37

華為拾貝

2012-07-11 13:54:42

網(wǎng)頁重構

2011-07-08 15:22:29

英特爾云計算

2012-02-01 13:54:54

Java游戲
點贊
收藏

51CTO技術棧公眾號