在開始之前,我們先限定下python解釋器的意思。當討論Python的時候,解釋器這個詞可以用在不同的地方。有的時候,解釋器指的是Python Interpreter,也就是你在命令行交互界面上輸入python的時候。有的時候人們或多或少的交換使用python和python解釋器來表明python從執行到結束的的過程。在本章中,解釋器有更加確切的意思:python執行過程中的最后一步。
在解釋器完成之前,python還有三個步驟需要執行:詞法分析,語法解釋,編譯。最后這些步驟講項目的源代碼從文本轉換成代碼對象,其中包含編譯器可以理解的指令。解釋器的工作就是讓這些代碼對象按照執行工作。
你可能很奇怪執行python也包含編譯這一步。Python通常和ruby以及perl一樣被稱為解釋型語言,它們和C,Rust這樣的編譯語言不一樣。盡管如此,這個術語並不像看起來那樣精確。多數解釋型語言,包括python,也有編譯的步驟。python被稱為解釋型語言的原因在於相對於編譯語言,編譯做了很少的工作,而解釋的工作更多。在后面的章節中能看到,Python解釋器比C語言編譯期需要更少的關於程序行為的信息。
Python的Python解釋器:
Byterun是一個用python寫的Python解釋器。這點可能會讓你感到奇怪,但是沒有什么比C語言寫C編譯器更奇怪的了(廣泛使用的C編譯器GCC就是用C語言寫的)。你可以用任務語言寫Python解釋器。
用Python寫Python解釋器有缺點也有優點。最大的缺點就是運行速度:在Byterun中執行代碼比在CPython中要慢得多。CPython就是用C語言寫的並且CPython做了認真的優化。盡管如此,Byterun被設計用於學習項目。所以速度對我們來說並不重要。最大的好處就是可以僅僅實現解釋器,而不用擔心雲信時部分,特別是對象系統。比如Byterun需要創建類時就會回退到真正的python.另外一個優點就是Byterun容易理解,特別是用高級別語言來實現,這使得很多人閱讀起來都很簡單。
構建一個解釋器:
在我們開始考察Byterun代碼前,我們需要在高層次上對解釋器的架構進行了解。python解釋器是如何工作的?
Python解釋器是一個虛擬機,是一個模擬真實計算機的軟件。這個虛擬機是一個棧機器;它操作幾個棧來實現操作(這和寄存器機器從內存位置去讀和寫不同)
Python解釋器是一個字節解釋器,它的輸入是被稱作為字節碼的指令集。當你寫Python的時候,詞法分析器,語法分析器和編譯器會生成代碼對象讓解釋器去工作。每一個代碼對象包含了待操作的指令集也就是字節碼,再加上解釋器需要的其他信息。字節碼是Python代碼的一個中間層:它表示了你按照解釋器能夠理解的方式來寫源代碼。這和匯編語言在C語言以及硬件之間作為中間表示的方式是類同的。
一個輕型的解釋器:
為了說明更具體,讓我們從一個小解釋器開始。這個解釋器只能做加法運算,並且只能理解3個指令。所有代碼的執行都包含了這3個指令的不同組合方式。3個指令如下:
LOAD_VALUE
-
ADD_TWO_VALUES
-
PRINT_ANSWER
假設7+5生成這樣的指令集:
what_to_execute = {
"instructions": [("LOAD_VALUE", 0), # the first number
("LOAD_VALUE", 1), # the second number
("ADD_TWO_VALUES", None),
("PRINT_ANSWER", None)],
"numbers": [7, 5] }
Python解釋器是一個棧機器,所以必須操作棧去完成2個數的加法。解釋器將從第一個指令LOAD_VALUE開始,然后將第一個數進棧。接下來第二個數字進棧。對於第三個指令:ADD_TWO_VALUES 將會讓2個數字出棧並且相加,然后將結果進棧。最后將結果出棧打印出來。
LOAD_VALUE指令告訴解釋器將一個數字進棧,但是單靠指令指出是哪一個數字。每一個指令都需要額外的信息來告訴解釋器去哪裝載數字。所以我們的指令集有兩部分:指令本身,加上指令需要的信息,告訴解釋器從哪里去找到數字裝載。
為什么不直接把數字放在指令里面呢?加入我們正在進行兩個字符串相加而不是2個數字。我們不太願意看到指令中充滿字符串,因為字符串占的空間都很大。這種設計也意味着我們只需要對象的一份拷貝。比如加法7+7,常量numbers
只需要存儲
7.
你可能會疑惑為什么會需要除了
ADD_TWO_VALUES
之外的所有指令。的確,對於兩個數相加的例子,有點人為制作的意思。但是這個指令卻可以制造更復雜的項目。比如,就目前我們定義的指令來說,只要給出正確的指令集合,我們可以進行
3
個數字的相加或者任意數字。同時棧提供了清晰的方法來追蹤解釋器的狀態,這會支持我們后續進行更復雜的應用。
現在我們開始自己寫解釋器
。解釋器對象都有一個用列表表示的棧。對象有一個方法來描述如何執行指令。比如
LOAD_VALUE,
解釋器就會將數字入棧。
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)
這
3
個函數實現了解釋器可以理解的
3
個指令。解釋器還需要一點:一個能把東西集合在一起並執行的方法。
這個方法就是
run_code,
它將前面定義的
what_to_execute
字典作為一個參數。循環所有的指令,處理傳給指令的參數,並且在解釋器對象里調用對應的方法。
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()
為了測試
,我們可以創建一個對象實例然后用前面定義的
7+5
的指令集來調用
run_code.
interpreter = Interpreter()
interpreter.run_code(what_to_execute)
可以肯定的是,打印結果為12。
雖然這個解釋器的功能有限,但是這確實是真正的Python解釋器做加法的過程。即使在這個小例子里面依然有許多東西需要特別提示
第一,一些指令需要參數。在真正的python的字節碼中,大約有一半的指令有參數。像我們的例子一樣,參數都是和指令打包在一起。值得注意的是指令的參數和被調用函數的參數是不一樣的。
第二,ADD_TWO_VALUES沒有要求任何參數。代替的是相加的參數從解釋器的棧中出棧。這就是基於棧解釋器的特點
記住使用正確的指令集合,不要對我們的解釋器做任何改變,我們可以一次性對超過2個的數字進行加法運算。假設指令集如下,你期待發生什么?如果你有一個友好的編譯器,你會寫什么樣的代碼來產生這個指令集。
what_to_execute = {
"instructions": [("LOAD_VALUE", 0),
("LOAD_VALUE", 1),
("ADD_TWO_VALUES", None),
("LOAD_VALUE", 2),
("ADD_TWO_VALUES", None),
("PRINT_ANSWER", None)],
"numbers": [7, 5, 8] }
從這點開始,我們看到這個結構體的擴展性:我們可以向解釋器對象增加方法來描述更多的操作(只要我們有一個編譯器組織好指令集)
變量
讓我們給解釋器來添加變量。我們需要一個指令來存儲變量值,STORE_NAME; 一個指令來獲取變量,LOAD_VALUE; 並且將變量名映射到值上。現在我們忽略命令空間和作用域,我們可以將變量名和映射直接存儲在解釋器對象上。最后,我們將保證what_to_execute有一個變量名列表和一個常量名列表。
>>> def s():
... a = 1
... b = 2
... print(a + b)
# a friendly compiler transforms `s` into:
what_to_execute = {
"instructions": [("LOAD_VALUE", 0),
("STORE_NAME", 0),
("LOAD_VALUE", 1),
("STORE_NAME", 1),
("LOAD_NAME", 0),
("LOAD_NAME", 1),
("ADD_TWO_VALUES", None),
("PRINT_ANSWER", None)],
"numbers": [1, 2],
"names": ["a", "b"] }
我們的新實現如下。為了跟蹤名字和數值之間的綁定關系,我們將在__init__方法中添加一個environmen字典。我們還會添加STORE_NAME和LOAD_NAME。 這些方法首先查找變量名稱然后使用字典去取出或者設置這個值。
現在指令參數就有2個不同的意思:它可以是numbers列表的索引,也可以是names列表的索引。通過檢查正在執行的指令,解釋器就可以知道是哪種參數。我們打破這個邏輯,將指令和參數的映射關系放入一個單獨的方法中去
class Interpreter:
def __init__(self):
self.stack = []
self.environment = {}
def STORE_NAME(self, name):
val = self.stack.pop()
self.environment[name] = val
def LOAD_NAME(self, name):
val = self.environment[name]
self.stack.append(val)
def parse_argument(self, instruction, argument, what_to_execute):
""" Understand what the argument to each instruction means."""
numbers = ["LOAD_VALUE"]
names = ["LOAD_NAME", "STORE_NAME"]
if instruction in numbers:
argument = what_to_execute["numbers"][argument]
elif instruction in names:
argument = what_to_execute["names"][argument]
return argument
def run_code(self, what_to_execute):
instructions = what_to_execute["instructions"]
for each_step in instructions:
instruction, argument = each_step
argument = self.parse_argument(instruction, argument, what_to_execute)
if instruction == "LOAD_VALUE":
self.LOAD_VALUE(argument)
elif instruction == "ADD_TWO_VALUES":
self.ADD_TWO_VALUES()
elif instruction == "PRINT_ANSWER":
self.PRINT_ANSWER()
elif instruction == "STORE_NAME":
self.STORE_NAME(argument)
elif instruction == "LOAD_NAME":
self.LOAD_NAME(argument)
僅僅有5個指令,run_code已經開始變得冗長了。如果我們保持這個結構,對於每條指令都需要增減一個if聲明。這里我們可以使用Python的動態查找。我們總會給一個FOO指令定義一個FOO方法,所以我們使用Python的getattr功能去查找對應的函數而不是使用if聲明。run_code方法如下:
def execute(self, what_to_execute):
instructions = what_to_execute["instructions"]
for each_step in instructions:
instruction, argument = each_step
argument = self.parse_argument(instruction, argument, what_to_execute)
bytecode_method = getattr(self, instruction)
if argument is None:
bytecode_method()
else:
bytecode_method(argument)
真正的
python
字節碼:
現在,我們將拋棄小的指令集,去看看真真的
Python
字節碼。字節碼的結構和解釋器的指令集有點類似,不同的是字節碼使用一個字節而不是一個長的名字去識別指令。我們通過一個簡短的函數來理解這種結構。
>>> def cond():
... x = 3
... if x < 5:
... return 'yes'
... else:
... return 'no'
...
Python
在運行的時候暴露很多內部信息,我們可以通過
REPL
來得到這些內部信息。對於函數對象
cond
,
cond.__code__
是對應的代碼對象,
cond.__code__.co_code
就是對應的字節碼。在你寫
python
代碼的時候,你覺得不想去用這些屬性。但是可以讓我們搞些惡作劇並且了解內部的結構。
>>> cond.__code__.co_code # the bytecode as raw bytes
b'd\x01\x00}\x00\x00|\x00\x00d\x02\x00k\x00\x00r\x16\x00d\x03\x00Sd\x04\x00Sd\x00
\x00S'
>>> list(cond.__code__.co_code) # the bytecode as numbers
[100, 1, 0, 125, 0, 0, 124, 0, 0, 100, 2, 0, 107, 0, 0, 114, 22, 0, 100, 3, 0, 83,
100, 4, 0, 83, 100, 0, 0, 83]
當我們直接打印這些字節碼的時候
,它看起來完全無法理解。我們唯一了解的就是一串字節碼。幸運的是我們有個強有力的工具來理解這些字節碼:
Python
標准庫的
dis
模塊
dis
是一個字節碼反匯編器。反匯編以給機器寫的底層代碼為輸入,像匯編語言和字節碼,然后以讓人可以理解的方式打印出來。當我們運行
dis.dis
的時候,會輸出每個字節碼的解釋。
>>> dis.dis(cond)
2 0 LOAD_CONST 1 (3)
3 STORE_FAST 0 (x)
3 6 LOAD_FAST 0 (x)
9 LOAD_CONST 2 (5)
12 COMPARE_OP 0 (<)
15 POP_JUMP_IF_FALSE 22
4 18 LOAD_CONST 3 ('yes')
21 RETURN_VALUE
6 >> 22 LOAD_CONST 4 ('no')
25 RETURN_VALUE
26 LOAD_CONST 0 (None)
29 RETURN_VALUE
這些都是什么意思
?讓我們來看第一個指令
LOAD_CONST,
第一列中的數字
2
代表了
Python
源代碼中的行號。第二列是字節碼的索引,表示
LOAD_CONST
指令出現在位置
0.
第三列就是可以供我們理解的指令本身。第四列如果有的話,就是指令的參數。第五列,如果存在的話就是關於參數是什么的指示。
現在來看下字節碼的前幾個字節
[100,1,0,125,0,0]
。 這
6
個字節代表了
2
個帶參數的指令。我們可以使用
dis.opname
將它們從字節映射到字符串上來看下
100
和
125
分別對應的的指令。
>>> dis.opname[100]
'LOAD_CONST'
>>> dis.opname[125]
'STORE_FAST'
第二和第三個字節
1,0
是
LOAD_CONST
的參數,第
4
個和第
5
個字節,
0,0
是
STORE_FAST
的參數。就像在你的小程序例子里面的一樣,
LOAD_CONST
需要知道到哪里去裝載常量,
STORE_FAST
需要知道需要找到存儲的名字。
(Python
的
LOAD_CONST
就像是我們小例子中的
LOAD_VALUE,LOAD_FAST
和
LOAD_NAME
一樣
)
。所以這
6
個字節代表了第一行
,x=3
。
(
為什么每個參數使用
2
個字節?如果
Python
使用一個字節來定位變量名而不是
2
個,那么一個代碼對象只能有
256
個名字
/
常量,使用
2
個字節,可以使用
256
或者
65536)
條件語句和循環語句
:
到目前為止
,解釋器根據指令一條一條的執行了代碼。這里有個問題;一般我們希望執行某條指令很多次,或者在特定條件下跳過它們。為了在代碼中寫循環,解釋器必須能在指令集中進行跳轉。
Python
在字節碼中使用
GOTO
聲明來處理條件語句和循環語句。再來看下
cond
功能的反匯編代碼
>>> dis.dis(cond)
2 0 LOAD_CONST 1 (3)
3 STORE_FAST 0 (x)
3 6 LOAD_FAST 0 (x)
9 LOAD_CONST 2 (5)
12 COMPARE_OP 0 (<)
15 POP_JUMP_IF_FALSE 22
4 18 LOAD_CONST 3 ('yes')
21 RETURN_VALUE
6 >> 22 LOAD_CONST 4 ('no')
25 RETURN_VALUE
26 LOAD_CONST 0 (None)
29 RETURN_VALUE
第
3
行的條件
if x<5
被匯編成了
4
條指令:
LOAD_FAST,LOAD_CONST,COMPARE_OP
以及
POP_JUMP_IF_FALSE. x<5
生成代碼去裝載
x
然后比較
2
個數字。
POP_JUMP_IF_FALSE
指令負責實現
If.
這個指令會將解釋器的棧頂元素出棧。如果這個值為真,那么什么都不會發生。如果這個值為
False
,解釋器將會跳轉到其他的指令。
這個將被加載的命令稱為跳轉目標
,並且當做參數提供給
POP_JUMP
指令。在這里跳轉目標是
22.
索引
22
的指令是在第
6
行的
LOAD_CONST(dis
通過
>>
來標記跳轉目標
)
。如果
if x<5
的結果是
False
,那么解釋器將會直接跳轉到第
6
行返回”
no”
,跳過第
4
行
(
返回“
yes”)
。所以,解釋器使用跳轉指令來選擇跳過部分指令集。
Python
的循環也是依賴與跳轉
。在下面的字節碼中,注意到
while x<5
行生成了幾乎和
if x<10
一樣的代碼。
在這兩個例子中,計算比較結果然后POP_JUMP_IF_FALSE
來控制下一步去要執行的指令。在第
4
行的結尾,也就是循環的結尾。指令
JUMP_ABSOLUTE
總是讓解釋器回到循環頂部的指令
9.
當
if x<5
變成
false
,
POP_JUMP_IF_FALSE
會讓解釋器跳轉到循環的結尾的地方也就是指令
34.
>>> def loop():
... x = 1
... while x < 5:
... x = x + 1
... return x
...
>>> dis.dis(loop)
2 0 LOAD_CONST 1 (1)
3 STORE_FAST 0 (x)
3 6 SETUP_LOOP 26 (to 35)
>> 9 LOAD_FAST 0 (x)
12 LOAD_CONST 2 (5)
15 COMPARE_OP 0 (<)
18 POP_JUMP_IF_FALSE 34
4 21 LOAD_FAST 0 (x)
24 LOAD_CONST 1 (1)
27 BINARY_ADD
28 STORE_FAST 0 (x)
31 JUMP_ABSOLUTE 9
>> 34 POP_BLOCK
5 >> 35 LOAD_FAST 0 (x)
38 RETURN_VALUE
探索字節碼
我建議你在自己寫的函數上運行
dis.dis
。一些問題值得探索:
1
對解釋器而言,
for
循環和
while
循環有什么不同
2
如何寫不同的函數但是產生同樣的字節碼
3 elif
是如何工作的?列表推導呢
幀
目前為止
,我們學習到了
Python
虛擬機是一個棧機器。它能順序的執行指令或者在指令集中跳轉,從棧中將元素出棧或出棧。但是和我們期望的還有很大的距離。在前面的例子中,最后一個指令是
RETURN_VALUE
對應到代碼中的
return
語句。但是這個返回值返回到哪去呢。
為了回答這個問題
,我們必須再增加一層復雜性:幀。一個棧是信息的集合以及代碼的上下文。幀在
Python
代碼執行的過程中創建和釋放。每個幀對應一次函數調用
-
所以一個幀只有一個對應的代碼對象,但是一個代碼對象有多個幀。如果你將一個函數自我調用
10
次,你將有
11
個幀
-
每層調用對應一個幀,另外一個就是你開始的模塊。總的來說,
Python
的每個作用域都有一個幀。比如,每個模塊,每個函數調用和類定義都有一個幀。
幀存在於調用棧中
,一個和我們之前討論的不同且復雜的棧
(
你最熟悉的就是調用棧
-
就是你經常看到的異常回溯。每個以“
File ‘programm.py”
開始的回溯對應一個幀
)
目前我們用到過的棧
-
解釋器在執行字節碼時操作的棧
-
我們稱為數據棧。其實還有第三個棧叫做塊棧。塊被用於特定的控制流,特別是循環和異常處理。調用棧上的每個幀都有屬於自己的數據棧和塊棧。
讓我們用一個具體的例子來說明下
。加入
Python
解釋器執行下面標記到
3
的地方。解釋器正處於函數
foo
的調用中,接下來正在調用
bar
。下面這個是幀調用棧,數據棧的結構圖。我們感興趣的是解釋器從底部的
foo
開始執行,接着執行
foo
,然后到
bar
>>> def bar(y):
... z = y + 3 # <--- (3) ... and the interpreter is here.
... return z
...
>>> def foo():
... a = 1
... b = 2
... return a + bar(b) # <--- (2) ... which is returning a call to bar ...
...
>>> foo() # <--- (1) We're in the middle of a call to foo ...
3
現在,解釋器處於函數bar的調用中。在調用棧中有3個幀:一個對應於模塊層,一個用於函數foo,一個用於函數bar。一旦bar 返回值,對應的幀就從調用棧中出棧並且丟棄。
字節碼指令RETURN_VALUE告訴解釋器在幀之間傳遞值。首先將會將調用棧頂部的幀的數據棧頂部數值出棧。然后將整個幀從調用棧中出棧並且丟棄。最后這個值在下一個幀的數據棧中入棧。
當我在寫Byterun實現的時候,很長一段時間我們的代碼一直存在一個錯誤。和每個幀有一個數據棧不同的是,我們在整個虛擬機上只有一個數據棧。我們寫了很多測試代碼並且在Byterun和真正的Python解釋器上去執行,期望得到同樣的結果。幾乎所有的測試都通過了。唯一不能工作的就是生成器。最后,仔細閱讀CPython的代碼,我們意思到了錯誤。將數據棧移到每個幀上解決了這個問題。
回頭看下這個bug,我們驚訝的發現Python其實很少依賴每個幀上有 一個數據棧的特性。Python解釋器中幾乎所有的操作都仔細的清理數據棧,所以在幀之間共享同樣的棧不會有太大的影響。在上面的例子中,當bar完成執行,它將會使自己的調用棧清空。即使foo共享這個棧,它的值也不會受影響。盡管如此,在使用生成器的時候,一個關鍵的特性就是在幀之間暫停,返回值到其他幀,過段時間又能返回到原來的幀。並以離開時相同的狀態執行。
Byterun
現在我們對Python解釋器有足夠的知識,我們可以開始來看下ByteRun的運行。
在ByteRun中有4種類型的對象:
虛擬機對象:用於控制最高層次的結構,特別是幀的調用棧,並且包含了指令到操作的映射。這是一個比之前解釋器更復雜的版本
幀對象:每個幀實例都有一個代碼對象並且管理一些其他必要的狀態位,特別是全局和本地命名空間,調用幀的引用,以及指令執行的最后一個字節碼
函數類:被用於真正的Python函數中。回想一下,調用一個函數會在解釋器上創建一個新的幀。我們實現Function來控制創建新的幀
塊類:它只是包裝了塊的3個屬性。(塊的具體實現不是Python解釋器的重點,所以我們不會在這上面花太多的時間,但它們被包含在內所以ByteRun可以運行真正的Python代碼)
虛擬機類:
程序運行的時候只有一個虛擬機類實例被創建,這是因為我們只有一個Python解釋器。虛擬機存儲調用棧,異常狀態,以及在幀之間傳遞的返回值。執行代碼的入口點是run_code函數,這個函數采用編譯的代碼對象作為參數。以創建幀開始然后運行。這個幀有可能創建其他的幀;調用棧隨着程序的運行增長和縮短。當第一個幀返回,執行就結束。
class VirtualMachineError(Exception):
pass
class VirtualMachine(object):
def __init__(self):
self.frames = [] # The call stack of frames.
self.frame = None # The current frame.
self.return_value = None
self.last_exception = None
def run_code(self, code, global_names=None, local_names=None):
""" An entry point to execute code using the virtual machine."""
frame = self.make_frame(code, global_names=global_names,
local_names=local_names)
self.run_frame(frame)
幀類:
接下來我們將寫幀對象。幀是一個屬性的集合,但是沒有方法。就像上面提到的,這些屬性包括編譯器創建的代碼對象;本地的,全局的以及命名空間;之前幀的引用;數據棧;塊棧;以及最后一個執行的指令。
class Frame(object):
def __init__(self, code_obj, global_names, local_names, prev_frame):
self.code_obj = code_obj
self.global_names = global_names
self.local_names = local_names
self.prev_frame = prev_frame
self.stack = []
if prev_frame:
self.builtin_names = prev_frame.builtin_names
else:
self.builtin_names = local_names['__builtins__']
if hasattr(self.builtin_names, '__dict__'):
self.builtin_names = self.builtin_names.__dict__
self.last_instruction = 0
self.block_stack = []
接下來,我們將在虛擬機上增加幀操作。這有3個幫助函數:創建新幀(負責為新幀尋找命令空間)和壓棧出棧的方法。第四個方法,run_frame, 完成執行幀的主要工作。我們待會來討論這個。
class VirtualMachine(object):
[... snip ...]
# Frame manipulation
def make_frame(self, code, callargs={}, global_names=None, local_names=None):
if global_names is not None and local_names is not None:
local_names = global_names
elif self.frames:
global_names = self.frame.global_names
local_names = {}
else:
global_names = local_names = {
'__builtins__': __builtins__,
'__name__': '__main__',
'__doc__': None,
'__package__': None,
}
local_names.update(callargs)
frame = Frame(code, global_names, local_names, self.frame)
return frame
def push_frame(self, frame):
self.frames.append(frame)
self.frame = frame
def pop_frame(self):
self.frames.pop()
if self.frames:
self.frame = self.frames[-1]
else:
self.frame = None
def run_frame(self):
pass
# we'll come back to this shortly
Function類
Function的實現有點曲折,但是大部分的細節對於理解解釋器並不重要。最值得注意的是解釋器調用方法的時候,會用到__call__方法—創建一個新的幀對象並且開始運行。
class Function(object):
"""
Create a realistic function object, defining the things the interpreter expects.
"""
__slots__ = [
'func_code', 'func_name', 'func_defaults', 'func_globals',
'func_locals', 'func_dict', 'func_closure',
'__name__', '__dict__', '__doc__',
'_vm', '_func',
]
def __init__(self, name, code, globs, defaults, closure, vm):
"""You don't need to follow this closely to understand the interpreter."""
self._vm = vm
self.func_code = code
self.func_name = self.__name__ = name or code.co_name
self.func_defaults = tuple(defaults)
self.func_globals = globs
self.func_locals = self._vm.frame.f_locals
self.__dict__ = {}
self.func_closure = closure
self.__doc__ = code.co_consts[0] if code.co_consts else None
# Sometimes, we need a real Python function. This is for that.
kw = {
'argdefs': self.func_defaults,
}
if closure:
kw['closure'] = tuple(make_cell(0) for _ in closure)
self._func = types.FunctionType(code, globs, **kw)
def __call__(self, *args, **kwargs):
"""When calling a Function, make a new frame and run it."""
callargs = inspect.getcallargs(self._func, *args, **kwargs)
# Use callargs to provide a mapping of arguments: values to pass into the new
# frame.
frame = self._vm.make_frame(
self.func_code, callargs, self.func_globals, {}
)
return self._vm.run_frame(frame)
def make_cell(value):
"""Create a real Python closure and grab a cell."""
# Thanks to Alex Gaynor for help with this bit of twistiness.
fn = (lambda x: lambda: x)(value)
return fn.__closure__[0]
接下來,回到VirtualMachine
對象
,我們將針對數據棧操作增加一些幫助方法。操作棧的字節碼總是在當前幀的數據棧上。這使得
POP_TOP,LOAD_FAST
以及其他操作棧的指令實現更加可讀
class VirtualMachine(object):
[... snip ...]
# Data stack manipulation
def top(self):
return self.frame.stack[-1]
def pop(self):
return self.frame.stack.pop()
def push(self, *vals):
self.frame.stack.extend(vals)
def popn(self, n):
"""Pop a number of values from the value stack.
A list of `n` values is returned, the deepest value first.
"""
if n:
ret = self.frame.stack[-n:]
self.frame.stack[-n:] = []
return ret
else:
return []
在我們運行幀之前
,我們還需要
2
個方法
第一
,
parse_byte_and_args
以一個字節碼輸入,檢查是否有參數,如果有的話就解析參數。這個方法同樣更新幀的屬性
last_instruction
,它指向最后執行的指令。一個指令如果沒有參數的話只有一個字節的長度,如果有參數的話就是
3
個字節;最后兩個字節是參數。每個指令參數的意義取決與指令。比如就像上面提到的,對於
POP_JUMP_IF_FALSE
,指令的參數意味着跳轉目標。對於
BUILD_LIST
,參數就表示列表中元素的個數。對於
LOAD_CONST
,參數表示常量列表的索引。
一些指令使用簡單的數字作為參數
。對於其他的指令,虛擬機還需要還要做一些工作去發現參數的意義。標准庫中的
dis
模塊有一個備用單,解釋各個參數的意義,這也使得我們的代碼更加簡潔。
比如,列表
dis.hasname
告訴我們
LOAD_NAME
、
IMPORT_NAME
、
LOAD_GLOBAL
,以及另外的
9
個指令的參數都有同樣的意義:對於這些指令,它們的參數代表了代碼對象中的名字列表的索引。
class VirtualMachine(object):
[... snip ...]
def parse_byte_and_args(self):
f = self.frame
opoffset = f.last_instruction
byteCode = f.code_obj.co_code[opoffset]
f.last_instruction += 1
byte_name = dis.opname[byteCode]
if byteCode >= dis.HAVE_ARGUMENT:
# index into the bytecode
arg = f.code_obj.co_code[f.last_instruction:f.last_instruction+2]
f.last_instruction += 2 # advance the instruction pointer
arg_val = arg[0] + (arg[1] * 256)
if byteCode in dis.hasconst: # Look up a constant
arg = f.code_obj.co_consts[arg_val]
elif byteCode in dis.hasname: # Look up a name
arg = f.code_obj.co_names[arg_val]
elif byteCode in dis.haslocal: # Look up a local name
arg = f.code_obj.co_varnames[arg_val]
elif byteCode in dis.hasjrel: # Calculate a relative jump
arg = f.last_instruction + arg_val
else:
arg = arg_val
argument = [arg]
else:
argument = []
return byte_name, argument
下一個函數是
dispatch
,這個函數用於查找給定指令的操作並且執行。在
CPython
解釋器中,這個函數用一個巨大的
1500
行的
switch
分之實現。幸運的是,我們在寫
Python
,我們可以寫的更簡潔。我們會為每個字節名字定義一個函數然后使用
getattr
去查找。就像上面的迷你解釋器一樣,如果我們的指令被命令為
FOO_BAR,
對應的函數名稱可以是
byte_FOO_BAR.
現在,我們將這些函數的具體實現看做是一個黑盒。每個字節碼的函數都會返回
None
或者稱作
why
的字符,有的時候解釋器需要這個額外的信息。這些指令函數的返回值就作為解釋器內部的狀態指示,不要把他們和幀返回結果相混淆。
class VirtualMachine(object):
[... snip ...]
def dispatch(self, byte_name, argument):
""" Dispatch by bytename to the corresponding methods.
Exceptions are caught and set on the virtual machine."""
# When later unwinding the block stack,
# we need to keep track of why we are doing it.
why = None
try:
bytecode_fn = getattr(self, 'byte_%s' % byte_name, None)
if bytecode_fn is None:
if byte_name.startswith('UNARY_'):
self.unaryOperator(byte_name[6:])
elif byte_name.startswith('BINARY_'):
self.binaryOperator(byte_name[7:])
else:
raise VirtualMachineError(
"unsupported bytecode type: %s" % byte_name
)
else:
why = bytecode_fn(*argument)
except:
# deal with exceptions encountered while executing the op.
self.last_exception = sys.exc_info()[:2] + (None,)
why = 'exception'
return why
def run_frame(self, frame):
"""Run a frame until it returns (somehow).
Exceptions are raised, the return value is returned.
"""
self.push_frame(frame)
while True:
byte_name, arguments = self.parse_byte_and_args()
why = self.dispatch(byte_name, arguments)
# Deal with any block management we need to do
while why and frame.block_stack:
why = self.manage_block_stack(why)
if why:
break
self.pop_frame()
if why == 'exception':
exc, val, tb = self.last_exception
e = exc(val)
e.__traceback__ = tb
raise e
return self.return_value
塊類
在我們實現每個字節碼的方法之前
,我們先簡短的討論下塊。塊被用於特定的流控制,特別是異常處理以及循環。塊確保當操作結束后數據棧是在正常的狀態。比如,在一個循環運行過程中,一個迭代對象保持在棧上,但是當迭代結束后卻出棧了。解釋器必須追蹤循環是結束了還是在繼續運行。
為了追蹤這些額外的信息
,解釋器設置了一個
flag
來標記循環的狀態。我們用稱作
why
的變量來實現這個標志位,標志位可以是
None
或者“
continue”,”break”,”exception”,”return”
之一。這個標記指示了塊棧以及數據棧應該執行的操作類型。回到這個迭代器的例子,如果塊棧的棧頂是
loop
塊而且
why
是
continue
,這個迭代器必須保持在數據棧上,但是如果
why
是
break
。那么迭代器就應該出棧。
塊操作的細節比這個還要繁瑣
,我們不會花太多的時間在這上面。感興趣的讀者可以自己下來閱讀。
Block = collections.namedtuple("Block", "type, handler, stack_height")
class VirtualMachine(object):
[... snip ...]
# Block stack manipulation
def push_block(self, b_type, handler=None):
stack_height = len(self.frame.stack)
self.frame.block_stack.append(Block(b_type, handler, stack_height))
def pop_block(self):
return self.frame.block_stack.pop()
def unwind_block(self, block):
"""Unwind the values on the data stack corresponding to a given block."""
if block.type == 'except-handler':
# The exception itself is on the stack as type, value, and traceback.
offset = 3
else:
offset = 0
while len(self.frame.stack) > block.level + offset:
self.pop()
if block.type == 'except-handler':
traceback, value, exctype = self.popn(3)
self.last_exception = exctype, value, traceback
def manage_block_stack(self, why):
""" """
frame = self.frame
block = frame.block_stack[-1]
if block.type == 'loop' and why == 'continue':
self.jump(self.return_value)
why = None
return why
self.pop_block()
self.unwind_block(block)
if block.type == 'loop' and why == 'break':
why = None
self.jump(block.handler)
return why
if (block.type in ['setup-except', 'finally'] and why == 'exception'):
self.push_block('except-handler')
exctype, value, tb = self.last_exception
self.push(tb, value, exctype)
self.push(tb, value, exctype) # yes, twice
why = None
self.jump(block.handler)
return why
elif block.type == 'finally':
if why in ('return', 'continue'):
self.push(self.return_value)
self.push(why)
why = None
self.jump(block.handler)
return why
return why
指令
現在剩下的工作就是去實現大量的指令方法了
。這些指令的實現並不有趣。所以我們只展示部分在這里。但是完整的實現在
GitHub
上。
(
這里包括的指令已經足夠實現我們上面的代碼了
)
class VirtualMachine(object):
[... snip ...]
## Stack manipulation
def byte_LOAD_CONST(self, const):
self.push(const)
def byte_POP_TOP(self):
self.pop()
## Names
def byte_LOAD_NAME(self, name):
frame = self.frame
if name in frame.f_locals:
val = frame.f_locals[name]
elif name in frame.f_globals:
val = frame.f_globals[name]
elif name in frame.f_builtins:
val = frame.f_builtins[name]
else:
raise NameError("name '%s' is not defined" % name)
self.push(val)
def byte_STORE_NAME(self, name):
self.frame.f_locals[name] = self.pop()
def byte_LOAD_FAST(self, name):
if name in self.frame.f_locals:
val = self.frame.f_loca
def byte_STORE_FAST(self, name):
self.frame.f_locals[name] = self.pop()
def byte_LOAD_GLOBAL(self, name):
f = self.frame
if name in f.f_globals:
val = f.f_globals[name]
elif name in f.f_builtins:
val = f.f_builtins[name]
else:
raise NameError("global name '%s' is not defined" % name)
self.push(val)
## Operators
BINARY_OPERATORS = {
'POWER': pow,
'MULTIPLY': operator.mul,
'FLOOR_DIVIDE': operator.floordiv,
'TRUE_DIVIDE': operator.truediv,
'MODULO': operator.mod,
'ADD': operator.add,
'SUBTRACT': operator.sub,
'SUBSCR': operator.getitem,
'LSHIFT': operator.lshift,
'RSHIFT': operator.rshift,
'AND': operator.and_,
'XOR': operator.xor,
'OR': operator.or_,
}
def binaryOperator(self, op):
x, y = self.popn(2)
self.push(self.BINARY_OPERATORS[op](x, y))
COMPARE_OPERATORS = [
operator.lt,
operator.le,
operator.eq,
operator.ne,
operator.gt,
operator.ge,
lambda x, y: x in y,
lambda x, y: x not in y,
lambda x, y: x is y,
lambda x, y: x is not y,
lambda x, y: issubclass(x, Exception) and issubclass(x, y),
]
def byte_COMPARE_OP(self, opnum):
x, y = self.popn(2)
self.push(self.COMPARE_OPERATORS[opnum](x, y))
## Attributes and indexing
def byte_LOAD_ATTR(self, attr):
obj = self.pop()
val = getattr(obj, attr)
self.push(val)
def byte_STORE_ATTR(self, name):
val, obj = self.popn(2)
setattr(obj, name, val)
## Building
def byte_BUILD_LIST(self, count):
elts = self.popn(count)
self.push(elts)
def byte_BUILD_MAP(self, size):
self.push({})
def byte_STORE_MAP(self):
the_map, val, key = self.popn(3)
the_map[key] = val
self.push(the_map)
def byte_LIST_APPEND(self, count):
val = self.pop()
the_list = self.frame.stack[-count] # peek
the_list.append(val)
## Jumps
def byte_JUMP_FORWARD(self, jump):
self.jump(jump)
def byte_JUMP_ABSOLUTE(self, jump):
self.jump(jump)
def byte_POP_JUMP_IF_TRUE(self, jump):
val = self.pop()
if val:
self.jump(jump)
def byte_POP_JUMP_IF_FALSE(self, jump):
val = self.pop()
if not val:
self.jump(jump)
## Blocks
def byte_SETUP_LOOP(self, dest):
self.push_block('loop', dest)
def byte_GET_ITER(self):
self.push(iter(self.pop()))
def byte_FOR_ITER(self, jump):
iterobj = self.top()
try:
v = next(iterobj)
self.push(v)
except StopIteration:
self.pop()
self.jump(jump)
def byte_BREAK_LOOP(self):
return 'break'
def byte_POP_BLOCK(self):
self.pop_block()
## Functions
def byte_MAKE_FUNCTION(self, argc):
name = self.pop()
code = self.pop()
defaults = self.popn(argc)
globs = self.frame.f_globals
fn = Function(name, code, globs, defaults, None, self)
self.push(fn)
def byte_CALL_FUNCTION(self, arg):
lenKw, lenPos = divmod(arg, 256) # KWargs not supported here
posargs = self.popn(lenPos)
func = self.pop()
frame = self.frame
retval = func(*posargs)
self.push(retval)
def byte_RETURN_VALUE(self):
self.return_value = self.pop()
return "return"
動態類型
,編譯器不知道它是什么
你可能聽過
Python
是一個動態的語言
,它是動態類型的。在我們構造解釋器的過程中,已經透露了這樣的信息。
動態的一個意思是很多工作是在運行的時候進行的
。前面我們看到
Python
編譯器沒有太多代碼在做什么的信息。比如,下面的
mod
函數。
mod
輸入是
2
個參數並且返回他們的模運算值。在字節碼中,我們看到變量
a
和
b
被加載,然后
BINARY_MODULD
進行模運算
>>> def mod(a, b):
... return a % b
>>> dis.dis(mod)
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_MODULO
7 RETURN_VALUE
>>> mod(19, 5)
4
計算
19%5
生成
4
,——下面不要感到奇怪,如果我們調用不同的參數會發生什么
>>> mod("by%sde", "teco")
'bytecode
剛才發生了什么
?你可能在其他地方見過這樣的語法。格式化字符串
>>> print("by%sde" % "teco")
bytecode
用符號
%
去格式化字符串會調用字節碼
BUNARY_MODULO
。它取棧頂的兩個值求模,不管這兩個值是字符串、數字或是你自己定義的類的實例。字節碼在函數編譯時生成(或者說,函數定義時)相同的字節碼會用於不同類的參數。
Python
的編譯器關於字節碼的功能知道得很少
。一切都取決於解釋器來決定
BINARY_MODULD
執行的對象類型並完成正確的操作。這就是
Python
被稱為動態類型:直到你運行的時候才會知道函數參數的類型。相反的是,靜態語言需要程序員告訴參數是什么類型
(
或者編譯器自己去找出參數的類型
)
解釋器的無知是
Python
的一個挑戰——比如字節碼
,不真正的運行代碼,你都不會直到指令將會干什么。事實上,你可以定義一個類來實現
__mode__
方法,當你在對象上使用
%
的時候
Python
就會調用這個方法。所以
BINARY_MODULD
可以運行任何代碼
看下下面的代碼
,第一個
a%b
簡直就是浪費
def mod(a,b):
a % b
return a %b
不幸的是
,對這段代碼做靜態分析,不運行它,就不能確定第一個
a%b
是否真的做了事情。
用
%
調用
__mod__
可能會寫一個文件,或是和程序的其他部分交互,或者其他任何可以在
Python
中完成的事。很難優化一個你不知道它會做什么的函數。在
Russell Power
和
Alex Rubinsteyn
的優秀論文中寫道,“我們可以用多快的速度解釋
Python
?”,他們說,“在普遍缺乏類型信息下,每條指令必須被看作一個
INVOKE_ARBITRARY_METHOD
總結
Byterun
是一個比
CPython
容易理解的簡潔的
Python
解釋器。
Byterun
復制了
CPython
的主要結構:一個基於棧的解釋器對稱之為字節碼的指令集進行操作,它們順序執行或在指令間跳轉,向棧中壓入和從中彈出數據。解釋器隨着函數和生成器的調用和返回,動態的創建、銷毀幀,並在幀之間跳轉。
Byterun
也有着和真正解釋器一樣的限制:因為
Python
使用動態類型,解釋器必須在運行時決定指令的正確行為。
我鼓勵你去反匯編你的程序,然后用 Byterun 來運行。你很快會發現這個縮短版的 Byterun 所沒有實現的指令。完整的實現在 https://github.com/nedbat/byterun,或者你可以仔細閱讀真正的 CPython 解釋器ceval.c
,你也可以實現自己的解釋器!