簡單理解Python解釋器
Python常見的解釋器
- CPython
該解釋器是Python官方的解釋器。使用C語言開發。在命令行中直接使用Python Shell就是使用的CPython解釋器。是使用最廣泛的解釋器 - IPython
IPython是基於CPython的一個交互式解釋器。只是在交互方式上進行了改進。底層運行還是CPython。IPython使用 ‘ In [序號]: ’ 作爲提示符。是不是很熟悉。安裝Anaconda後,附帶的Jupyter NoteBook就是使用此解釋器。 - PyPy
PyPy以運行速度爲目的。顯著的提高了執行速度。與CPython有所不同。所以有可能產生錯誤。 - Jython
運行在Java平臺的Python解釋器。將代碼編譯成Java字節碼運行。 - IronPython
運行在微軟.Net平臺上的Python解釋器。編譯成.Net的字節碼。
解釋器幹啥了?
解釋器將代碼編譯成字節碼執行。Python之所以被稱爲解釋型語言,只是因爲他在編譯上的工作比重小得多。其他大多數解釋型語言也都類似。
一個簡單的解釋器
我們旨在實現字節碼運行的實現。我們假設程序 " 7 + 5 " 的指令集和如下(事實上與實際十分類似,在後面的內容將會看到):
what_to_execute = {
"instructions": [("LOAD_VALUE", 0), # 第一個數
("LOAD_VALUE", 1), # 第二個數
("ADD_TWO_VALUES", None),
("PRINT_ANSWER", None)],
"numbers": [7, 5] }
我們的解釋器基於棧來實現。指令 “LOAD_VALUE” 將第一個數壓入棧中,指令 “ADD_TWO_VALUES” 從棧頂順序彈出兩個數相加後壓入棧中,指令 “PRINT_ANSWER” 將棧頂元素彈出。
這裏使用列表來模擬棧,實現每個指令對應的函數,代碼如下:
class Interpreter:
def __init__(self):
self.stack = []
def LOAD_VALUE(self, number):
self.stack.append(number)
def PRINT_ANSWER(self):
answer = self.stack.pop()
print(answer)
def ADD_TWO_VALUES(self):
first_num = self.stack.pop()
second_num = self.stack.pop()
total = first_num + second_num
self.stack.append(total)
def run_code(self, what_to_execute):
#指令列表
instructions = what_to_execute["instructions"]
#常數列表
numbers = what_to_execute["numbers"]
#遍歷指令列表,一個一個執行
for each_step in instructions:
#得到指令和對應參數
instruction, argument = each_step
if instruction == "LOAD_VALUE":
number = numbers[argument]
self.LOAD_VALUE(number)
elif instruction == "ADD_TWO_VALUES":
self.ADD_TWO_VALUES()
elif instruction == "PRINT_ANSWER":
self.PRINT_ANSWER()
interpreter = Interpreter()
interpreter.run_code(what_to_execute)
實際的字節碼
- 使用__code__.co_code查看字節碼
進入Python交互式命令行,參考代碼查看:
>>> def cond():
... x = 3
... if x < 5:
... return 'yes'
... else:
... return 'no'
...
>>> cond.__code__.co_code
b'd\x01}\x00|\x00d\x02k\x00r\x10d\x03S\x00d\x04S\x00d\x00S\x00'
>>> list(bytearray(cond.__code__.co_code))
[100, 1, 125, 0, 124, 0, 100, 2, 107, 0, 114, 16, 100, 3, 83, 0, 100, 4, 83, 0, 100, 0, 83, 0]
- 使用dis模塊查看
標準庫中的dis模塊,可以實現字節碼的反彙編。將字節碼以人類可讀的方式輸出
>>> import dis
>>> dis.dis(cond)
2 0 LOAD_CONST 1 (3)
2 STORE_FAST 0 (x)
3 4 LOAD_FAST 0 (x)
6 LOAD_CONST 2 (5)
8 COMPARE_OP 0 (<)
10 POP_JUMP_IF_FALSE 16
4 12 LOAD_CONST 3 ('yes')
14 RETURN_VALUE
6 >> 16 LOAD_CONST 4 ('no')
18 RETURN_VALUE
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
輸出分爲5列,分別代表:字節碼對應在源碼中的行號,字節碼在字節碼串中的第幾個字節,字節碼人類可讀的命名,字節碼參數,字節碼參數的內容
參考輸出我們來理解一哈:
2 0 LOAD_CONST 1 (3) # 加載常量
2 STORE_FAST 0 (x) # 變量名初始化
3 4 LOAD_FAST 0 (x) # 加載變量
6 LOAD_CONST 2 (5)
8 COMPARE_OP 0 (<) # 彈出棧頂的兩個值作小於比較
10 POP_JUMP_IF_FALSE 16 # 結果爲假跳轉到 16 執行,爲真順序執行
4 12 LOAD_CONST 3 ('yes')
14 RETURN_VALUE
6 >> 16 LOAD_CONST 4 ('no')
18 RETURN_VALUE
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
我們再來看看循環的樣子:
>>> def loop():
... x = 1
... while x < 5:
... x = x + 1
... return x
...
>>> dis.dis(loop)
2 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (x)
3 4 SETUP_LOOP 20 (to 26) # 開始循環的標誌,在20 到 26 之間循環
>> 6 LOAD_FAST 0 (x)
8 LOAD_CONST 2 (5)
10 COMPARE_OP 0 (<)
12 POP_JUMP_IF_FALSE 24 # 循環退出的標誌,跳轉 24
4 14 LOAD_FAST 0 (x)
16 LOAD_CONST 1 (1)
18 BINARY_ADD
20 STORE_FAST 0 (x)
22 JUMP_ABSOLUTE 6 # 跳出循環塊
>> 24 POP_BLOCK
5 >> 26 LOAD_FAST 0 (x)
28 RETURN_VALUE
>>>
幀
- 通過以上的內容我們已經知道了一個函數內的字節碼是如何工作的。那函數之間的呢。這裏我們引入一個概念:“幀”。幀包含了一端代碼運行時所需的信息與上下文環境。在代碼執行時動態的創建與銷燬。每一個幀對應一次函數的調用。因爲一個函數可以遞歸調用自己多次,所以一個code object可以擁有多個幀。
- 解釋器中常用的兩種棧,一種是數據棧,執行字節碼操作時使用,上文已經涉及。還有一種叫塊棧,用於特定的控制流(循環,異常處理),每一個幀都有自己的數據棧和塊棧。
- 在多個函數之間每個函數運行開始時,將函數對應的幀壓入棧中(函數的調用棧),結束時將對應的幀彈出,並將return value的值壓入下一個幀的數據棧中。就完成了一次函數間的值傳遞。
總結一哈
python解釋器包含常用的三種棧,調用棧,數據棧,塊棧。
-
調用棧的運行
解釋器首先將源碼編譯爲字節碼,創建調用棧,以創建第一個幀開始運行,在這之後不斷的新建幀,或彈出幀並拿到返回值,調用棧的長度隨之變化,直到第一個創建幀返回值,運行結束。 -
幀的運行
- 每一個幀都有對應的code_object,一條沒有參數的指令佔據一個字節,有參數的指令佔據三個字節,後兩個字節爲參數。這裏的參數因指令的不同而效果不同,比如指令POP_JUMP_IF_FALSE,它的參數指的是跳轉目標。BUILD_LIST, 它的參數是列表的個數。LOAD_CONST,它的參數是常量的索引。
- 在一個幀運行時,在一個循環中順序執行每一條指令(使用指令名和參數完成具體操作)。一個標記量標記當前運行到字節碼的哪個位置。直到捕獲到異常或運行完所有指令拿到返回值。