Python 實現 Python 解釋器
本課程由ekCit發布在實驗樓,完整教程及在線練習地址:Python 實現 Python 解釋器
一、課程介紹
1. 課程來源
本課程核心部分來自《500 lines or less》項目,作者是 Dropbox 的 Allison Kaptur。項目代碼使用 MIT 協議,項目文檔使用 Creative Commons Legal Code 協議。
課程內容在原文檔基礎上做了稍許修改,增加了部分原理介紹,步驟的拆解分析及源代碼注釋。
2. 內容簡介
本課程會從實現一個玩具解釋器開始學習實現解釋器需要的基本知識。之後通過Python的dis庫查看真實的Python位元組碼,進一步理解Python解釋器的內部機制。最終參考Byterun(一個現有的Python解釋器)實現一個500行以內的Python解釋器。
課程來源的原作者 Allison Kaptur 就是Byterun的主要作者之一。Byterun的結構與CPython很像,理解Byterun能夠幫助你理解大部分解釋器,對於理解CPython更是大有幫助。
3. 課程知識點
本課程項目完成過程中,我們將學習:
- Python程序的運行原理
- Python解釋器的內部機制
- 如何實現一個Python解釋器
- 一些編寫Python程序的小技巧
二、實驗說明
1. Python解釋器
這裡的Python解釋器具體是指什麼呢?有時候我們會把Python的REPL(命令行下Python的交互環境)當作解釋器,有時候Python解釋器這一說法可以指代整個Python,它會將源代碼編譯為位元組碼並執行。本課程實現的解釋器只完成最後一部分執行位元組碼的工作,也就相當於一個跑Python位元組碼的Python虛擬機。
你也許會奇怪Python不是解釋型語言嗎,虛擬機跑位元組碼那不就像java那種編譯型語言了嗎。其實這種分類本來就不是很精確的,大部分的解釋型語言包括Python都會有編譯這個過程。之所以被稱作是解釋型語言是因為它們在編譯上的工作比重相對而言小很多。
2. Python實現的Python解釋器
本課程的原型-Byterun是一個Python實現的Python解釋器,你也許會覺得很奇怪,好比自己生下了自己這種說法一樣奇怪。其實也沒那麼奇怪,你看gcc就是用 C 寫的,你也可以使用別的語言來實現python解釋器,其實除了實現的功能之外,解釋器跟一般的程序並沒有什麼不同。
使用Python實現Python解釋器有優點也有缺點,最大的缺點就是速度,Byterun運行python程序會比CPython慢很多。優點就是我們可以直接使用Python的部分原生實現,比如Python的對象系統。當Byterun需要創建一個類的時候,可以直接使用原Python進行創建。當然最大的優點還是Python代碼短小精悍,僅僅500行就能實現一個功能還算完整的解釋器,所以說人生苦短,Python是岸吶。
3. Python解釋器的結構
我們的Python解釋器是一個模擬堆棧機器的虛擬機,僅使用多個棧來完成操作。解釋器所處理的位元組碼來自於對源代碼進行詞法分析、語法分析和編譯後所生成的code object中的指令集合。它相當於Python代碼的一個中間層表示,好比彙編代碼之於C代碼。
三、Hello, 解釋器
讓我們從解釋器界的hello world開始吧,這個最簡單的入門解釋器僅實現加法這一個功能。它也只認得三條指令,所以它可以運行的程序也只有這三個指令的排列組合而已。現在聽上去挺寒顫的,但在完成本課程的學習後就不一樣啦。
入門解釋器最基本的三個指令:
- LOAD_VALUE
- ADD_TWO_VALUES
- PRINT_ANSWER
既然我們只關心運行位元組碼的部分,那就不用去管源代碼是如何編譯為上述三個指令的某種排列組合的。我們只要照著編譯後的內容逐指令運行就行了。從另一個方面看,你要是發明了一種新語言,同時編寫了相應的生成位元組碼的編譯器,那就可以在我們的python解釋器上跑了呀。
以 7 + 5 作為源碼舉例, 編譯後生成以下指令集合:
what_to_execute = {n "instructions": [("LOAD_VALUE", 0), # 第一個數n ("LOAD_VALUE", 1), # 第二個數n ("ADD_TWO_VALUES", None),n ("PRINT_ANSWER", None)],n "numbers": [7, 5] }n
在這裡what_to_execute相當於code object, instructions相當於位元組碼。
我們的解釋器是一個堆棧機器,所以是使用棧來完成加法的。首先執行第一個指令 LOAD_VALUE,將第一個數壓入棧中,第二個指令同樣將第二個數壓入棧中。第三個指令 ADD_TWO_VALUES 彈出棧中的兩個數,將它們相加並將結果壓入棧中,最後一個指令彈出棧中的答案並列印。棧的內容變化如下圖所示:
LOAD_VALUE 指令需要找到參數指定的數據進行壓棧,那麼數據哪裡來的呢?可以發現我們的指令集包含兩部分:指令自身與一個常量列表。數據來自常量列表。n了解了這些後來寫我們的解釋器程序。我們使用列表來表示棧,同時編寫指令相應的方法模擬指令的運行效果。
class Interpreter:n def __init__(self):n self.stack = []nn def LOAD_VALUE(self, number):n self.stack.append(number)nn def PRINT_ANSWER(self):n answer = self.stack.pop()n print(answer)nn def ADD_TWO_VALUES(self):n first_num = self.stack.pop()n second_num = self.stack.pop()n total = first_num + second_numn self.stack.append(total)n
編寫輸入指令集合然後逐指令執行的方法:
def run_code(self, what_to_execute):n #指令列表n instructions = what_to_execute["instructions"]n #常數列表n numbers = what_to_execute["numbers"]n #遍歷指令列表,一個一個執行n for each_step in instructions:n #得到指令和對應參數n instruction, argument = each_stepn if instruction == "LOAD_VALUE":n number = numbers[argument]n self.LOAD_VALUE(number)n elif instruction == "ADD_TWO_VALUES":n self.ADD_TWO_VALUES()n elif instruction == "PRINT_ANSWER":n self.PRINT_ANSWER()n
測試一下
interpreter = Interpreter()ninterpreter.run_code(what_to_execute)n
運行結果:
儘管我們的解釋器現在還很弱,但它執行指令的過程跟真實Python實際上是差不多的,代碼里有幾個需要注意的地方:n- 代碼中LOAD_VALUE方法的參數是已讀取的常量而不是指令的參數。
- ADD_TWO_VALUES 並不需要任何參數,計算使用的數直接從棧中彈出獲得,這也是基於棧的解釋器的特性。
我們可以利用現有的指令運行3個數甚至多個數的加法:
what_to_execute = {n "instructions": [("LOAD_VALUE", 0),n ("LOAD_VALUE", 1),n ("ADD_TWO_VALUES", None),n ("LOAD_VALUE", 2),n ("ADD_TWO_VALUES", None),n ("PRINT_ANSWER", None)],n "numbers": [7, 5, 8] }n
運行結果:
變數n下一步我們要在我們的解釋器中加入變數這個概念,因此需要新增兩個指令:
- STORE_NAME: 存儲變數值,將棧頂的內容存入變數中。
- LOAD_NAME: 讀取變數值,將變數的內容壓棧。
以及新增一個變數名列表。
下面是我們需要運行的指令集合:
#源代碼ndef s():n a = 1n b = 2n print(a + b)nn#編譯後的位元組碼nwhat_to_execute = {n "instructions": [("LOAD_VALUE", 0),n ("STORE_NAME", 0),n ("LOAD_VALUE", 1),n ("STORE_NAME", 1),n ("LOAD_NAME", 0),n ("LOAD_NAME", 1),n ("ADD_TWO_VALUES", None),n ("PRINT_ANSWER", None)],n "numbers": [1, 2],n "names": ["a", "b"] }n
因為這裡不考慮命名空間和作用域的問題,所以在實現解釋器的時候可以直接將變數與常量的映射關係以字典的形式存儲在解釋n器對象的成員變數中,同時由於多了變數名列表與操作變數名列表的指令,通過指令參數取得方法參數的時候還需根據指令來判斷所取的是哪一個列表(常量列表還n是變數名列表),因此需要再實現一個解析指令參數的方法。
帶有變數的解釋器的代碼實現如下:
class Interpreter:n def __init__(self):n self.stack = []n #存儲變數映射關係的字典變數n self.environment = {}nn def STORE_NAME(self, name):n val = self.stack.pop()n self.environment[name] = valnn def LOAD_NAME(self, name):n val = self.environment[name]n self.stack.append(val)nn def LOAD_VALUE(self, number):n self.stack.append(number)nn def PRINT_ANSWER(self):n answer = self.stack.pop()n print(answer)nn def ADD_TWO_VALUES(self):n first_num = self.stack.pop()n second_num = self.stack.pop()n total = first_num + second_numn self.stack.append(total)nn def parse_argument(self, instruction, argument, what_to_execute):n #解析命令參數n #使用常量列表的方法n numbers = ["LOAD_VALUE"]n #使用變數名列表的方法n names = ["LOAD_NAME", "STORE_NAME"]nn if instruction in numbers:n argument = what_to_execute["numbers"][argument]n elif instruction in names:n argument = what_to_execute["names"][argument]nn return argumentnn def run_code(self, what_to_execute):n instructions = what_to_execute["instructions"]n for each_step in instructions:n instruction, argument = each_stepn argument = self.parse_argument(instruction, argument, what_to_execute)nn if instruction == "LOAD_VALUE":n self.LOAD_VALUE(argument)n elif instruction == "ADD_TWO_VALUES":n self.ADD_TWO_VALUES()n elif instruction == "PRINT_ANSWER":n self.PRINT_ANSWER()n elif instruction == "STORE_NAME":n self.STORE_NAME(argument)n elif instruction == "LOAD_NAME":n self.LOAD_NAME(argument)n
運行結果:
相信你已經發現,我們現在才實現五個指令,然而run_code已經看上去有點"腫"了,之後再追加新的指令,它就更"腫"了。不怕,可以利用python的動態方法查找特性。因為指令名與對應的實現方法名是相同的,所以可以利用getattr方法,getattr會根據輸入的方法名返回對應的方法,這樣就可以擺脫臃腫的分支結構,同時再追加新指令也不用修改原來的run_code代碼了。n
下面是run_code的進化版execute:
def execute(self, what_to_execute):n instructions = what_to_execute["instructions"]n for each_step in instructions:n instruction, argument = each_stepn argument = self.parse_argument(instruction, argument, what_to_execute)n bytecode_method = getattr(self, instruction)n if argument is None:n bytecode_method()n else:n bytecode_method(argument)n
本節課就到這裡啦,在這節課里我們實現了一個玩具解釋器,它運行的是我們自己定義的code object結構,下節課我們就會接觸到真實Python的code object與位元組碼了,下節課見~
實驗二 真實的Python位元組碼
實驗三 實現Python解釋器
實驗二、三可以在實驗樓中在線完成,立即【開始實驗】。
更多Python經典項目:Python全部 - 課程
推薦閱讀:
※如何開發Python第三方庫?
※python網頁爬蟲是非法的嗎?
※想知道大家都用python寫過哪些有趣的腳本?
※Python基本語法學完了,接下來不知道要幹什麼?