Python Interpreter


 

在開始之前,我們先限定下python解釋器的意思。當討論Python的時候,解釋器這個詞可以用在不同的地方。有的時候,解釋器指的是Python Interpreter,也就是你在命令行交互界面上輸入python的時候。有的時候人們或多或少的交換使用pythonpython解釋器來表明python從執行到結束的的過程。在本章中,解釋器有更加確切的意思:python執行過程中的最后一步。

 


 

在解釋器完成之前,python還有三個步驟需要執行:詞法分析,語法解釋,編譯。最后這些步驟講項目的源代碼從文本轉換成代碼對象,其中包含編譯器可以理解的指令。解釋器的工作就是讓這些代碼對象按照執行工作。

 


 

你可能很奇怪執行python也包含編譯這一步。Python通常和ruby以及perl一樣被稱為解釋型語言,它們和CRust這樣的編譯語言不一樣。盡管如此,這個術語並不像看起來那樣精確。多數解釋型語言,包括python,也有編譯的步驟。python被稱為解釋型語言的原因在於相對於編譯語言,編譯做了很少的工作,而解釋的工作更多。在后面的章節中能看到,Python解釋器比C語言編譯期需要更少的關於程序行為的信息。

 


 

PythonPython解釋器:

 

Byterun是一個用python寫的Python解釋器。這點可能會讓你感到奇怪,但是沒有什么比C語言寫C編譯器更奇怪的了(廣泛使用的C編譯器GCC就是用C語言寫的)。你可以用任務語言寫Python解釋器。

 


 

PythonPython解釋器有缺點也有優點。最大的缺點就是運行速度:在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_NAMELOAD_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方法,所以我們使用Pythongetattr功能去查找對應的函數而不是使用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來得到這些內部信息。對於函數對象condcond.__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將它們從字節映射到字符串上來看下100125分別對應的的指令。

>>> dis.opname[100]
'LOAD_CONST'
>>> dis.opname[125]
'STORE_FAST'
第二和第三個字節1,0LOAD_CONST的參數,第4個和第5個字節,0,0STORE_FAST的參數。就像在你的小程序例子里面的一樣,LOAD_CONST需要知道到哪里去裝載常量,STORE_FAST需要知道需要找到存儲的名字。(PythonLOAD_CONST就像是我們小例子中的LOAD_VALUE,LOAD_FASTLOAD_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變成falsePOP_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_NAMEIMPORT_NAMELOAD_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塊而且whycontinue,這個迭代器必須保持在數據棧上,但是如果whybreak。那么迭代器就應該出棧。


塊操作的細節比這個還要繁瑣,我們不會花太多的時間在這上面。感興趣的讀者可以自己下來閱讀。

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個參數並且返回他們的模運算值。在字節碼中,我們看到變量ab被加載,然后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,你也可以實現自己的解釋器!

 
         
        
 
 
         
        
 

 

 

 
         
        
 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM