在開始之前,我們先限定下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_numself.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_stepif 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] = valdef 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 argumentdef run_code(self, what_to_execute):instructions = what_to_execute["instructions"]for each_step in instructions:instruction, argument = each_stepargument = 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_stepargument = 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 bytesb'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 224 18 LOAD_CONST 3 ('yes')21 RETURN_VALUE6 >> 22 LOAD_CONST 4 ('no')25 RETURN_VALUE26 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 224 18 LOAD_CONST 3 ('yes')21 RETURN_VALUE6 >> 22 LOAD_CONST 4 ('no')25 RETURN_VALUE26 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 344 21 LOAD_FAST 0 (x)24 LOAD_CONST 1 (1)27 BINARY_ADD28 STORE_FAST 0 (x)31 JUMP_ABSOLUTE 9>> 34 POP_BLOCK5 >> 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):passclass VirtualMachine(object):def __init__(self):self.frames = [] # The call stack of frames.self.frame = None # The current frame.self.return_value = Noneself.last_exception = Nonedef 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_objself.global_names = global_namesself.local_names = local_namesself.prev_frame = prev_frameself.stack = []if prev_frame:self.builtin_names = prev_frame.builtin_nameselse:self.builtin_names = local_names['__builtins__']if hasattr(self.builtin_names, '__dict__'):self.builtin_names = self.builtin_names.__dict__self.last_instruction = 0self.block_stack = []
接下來,我們將在虛擬機上增加幀操作。這有3個幫助函數:創建新幀(負責為新幀尋找命令空間)和壓棧出棧的方法。第四個方法,run_frame, 完成執行幀的主要工作。我們待會來討論這個。
class VirtualMachine(object):[... snip ...]# Frame manipulationdef 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_nameselif self.frames:global_names = self.frame.global_nameslocal_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 framedef push_frame(self, frame):self.frames.append(frame)self.frame = framedef pop_frame(self):self.frames.pop()if self.frames:self.frame = self.frames[-1]else:self.frame = Nonedef 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 = vmself.func_code = codeself.func_name = self.__name__ = name or code.co_nameself.func_defaults = tuple(defaults)self.func_globals = globsself.func_locals = self._vm.frame.f_localsself.__dict__ = {}self.func_closure = closureself.__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 manipulationdef 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 retelse: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.frameopoffset = f.last_instructionbyteCode = f.code_obj.co_code[opoffset]f.last_instruction += 1byte_name = dis.opname[byteCode]if byteCode >= dis.HAVE_ARGUMENT:# index into the bytecodearg = f.code_obj.co_code[f.last_instruction:f.last_instruction+2]f.last_instruction += 2 # advance the instruction pointerarg_val = arg[0] + (arg[1] * 256)if byteCode in dis.hasconst: # Look up a constantarg = f.code_obj.co_consts[arg_val]elif byteCode in dis.hasname: # Look up a namearg = f.code_obj.co_names[arg_val]elif byteCode in dis.haslocal: # Look up a local namearg = f.code_obj.co_varnames[arg_val]elif byteCode in dis.hasjrel: # Calculate a relative jumparg = f.last_instruction + arg_valelse:arg = arg_valargument = [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 = Nonetry: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 whydef 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 dowhile why and frame.block_stack:why = self.manage_block_stack(why)if why:breakself.pop_frame()if why == 'exception':exc, val, tb = self.last_exceptione = exc(val)e.__traceback__ = tbraise ereturn 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 manipulationdef 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 = 3else:offset = 0while 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, tracebackdef manage_block_stack(self, why):""" """frame = self.frameblock = frame.block_stack[-1]if block.type == 'loop' and why == 'continue':self.jump(self.return_value)why = Nonereturn whyself.pop_block()self.unwind_block(block)if block.type == 'loop' and why == 'break':why = Noneself.jump(block.handler)return whyif (block.type in ['setup-except', 'finally'] and why == 'exception'):self.push_block('except-handler')exctype, value, tb = self.last_exceptionself.push(tb, value, exctype)self.push(tb, value, exctype) # yes, twicewhy = Noneself.jump(block.handler)return whyelif block.type == 'finally':if why in ('return', 'continue'):self.push(self.return_value)self.push(why)why = Noneself.jump(block.handler)return whyreturn why
指令
現在剩下的工作就是去實現大量的指令方法了。這些指令的實現並不有趣。所以我們只展示部分在這里。但是完整的實現在GitHub上。(這里包括的指令已經足夠實現我們上面的代碼了)
class VirtualMachine(object):[... snip ...]## Stack manipulationdef byte_LOAD_CONST(self, const):self.push(const)def byte_POP_TOP(self):self.pop()## Namesdef byte_LOAD_NAME(self, name):frame = self.frameif 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.frameif 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)## OperatorsBINARY_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 indexingdef 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)## Buildingdef 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] = valself.push(the_map)def byte_LIST_APPEND(self, count):val = self.pop()the_list = self.frame.stack[-count] # peekthe_list.append(val)## Jumpsdef 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)## Blocksdef 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()## Functionsdef byte_MAKE_FUNCTION(self, argc):name = self.pop()code = self.pop()defaults = self.popn(argc)globs = self.frame.f_globalsfn = Function(name, code, globs, defaults, None, self)self.push(fn)def byte_CALL_FUNCTION(self, arg):lenKw, lenPos = divmod(arg, 256) # KWargs not supported hereposargs = self.popn(lenPos)func = self.pop()frame = self.frameretval = 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_MODULO7 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 % breturn 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,你也可以實現自己的解釋器!

