簡單的理解python解釋器的運行機制

簡單理解Python解釋器


Python常見的解釋器

  1. CPython
    該解釋器是Python官方的解釋器。使用C語言開發。在命令行中直接使用Python Shell就是使用的CPython解釋器。是使用最廣泛的解釋器
  2. IPython
    IPython是基於CPython的一個交互式解釋器。只是在交互方式上進行了改進。底層運行還是CPython。IPython使用 ‘ In [序號]: ’ 作爲提示符。是不是很熟悉。安裝Anaconda後,附帶的Jupyter NoteBook就是使用此解釋器。
  3. PyPy
    PyPy以運行速度爲目的。顯著的提高了執行速度。與CPython有所不同。所以有可能產生錯誤。
  4. Jython
    運行在Java平臺的Python解釋器。將代碼編譯成Java字節碼運行。
  5. 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)

實際的字節碼

  1. 使用__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]
  1. 使用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                      
>>>                                               

  1. 通過以上的內容我們已經知道了一個函數內的字節碼是如何工作的。那函數之間的呢。這裏我們引入一個概念:“幀”。幀包含了一端代碼運行時所需的信息與上下文環境。在代碼執行時動態的創建與銷燬。每一個幀對應一次函數的調用。因爲一個函數可以遞歸調用自己多次,所以一個code object可以擁有多個幀。
  2. 解釋器中常用的兩種棧,一種是數據棧,執行字節碼操作時使用,上文已經涉及。還有一種叫塊棧,用於特定的控制流(循環,異常處理),每一個幀都有自己的數據棧和塊棧。
  3. 在多個函數之間每個函數運行開始時,將函數對應的幀壓入棧中(函數的調用棧),結束時將對應的幀彈出,並將return value的值壓入下一個幀的數據棧中。就完成了一次函數間的值傳遞。

總結一哈

python解釋器包含常用的三種棧,調用棧,數據棧,塊棧。

  • 調用棧的運行
    解釋器首先將源碼編譯爲字節碼,創建調用棧,以創建第一個幀開始運行,在這之後不斷的新建幀,或彈出幀並拿到返回值,調用棧的長度隨之變化,直到第一個創建幀返回值,運行結束。

  • 幀的運行

  1. 每一個幀都有對應的code_object,一條沒有參數的指令佔據一個字節,有參數的指令佔據三個字節,後兩個字節爲參數。這裏的參數因指令的不同而效果不同,比如指令POP_JUMP_IF_FALSE,它的參數指的是跳轉目標。BUILD_LIST, 它的參數是列表的個數。LOAD_CONST,它的參數是常量的索引。
  2. 在一個幀運行時,在一個循環中順序執行每一條指令(使用指令名和參數完成具體操作)。一個標記量標記當前運行到字節碼的哪個位置。直到捕獲到異常或運行完所有指令拿到返回值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章