大牛教你如何用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程式碼。
4、Hello, 直譯器
讓我們從直譯器界的hello world開始吧,這個最簡單的入門直譯器僅實現加法這一個功能。它也只認得三條指令,所以它可以執行的程式也只有這三個指令的排列組合而已。現在聽上去挺寒顫的,但在完成本課程的學習後就不一樣啦。
入門直譯器最基本的三個指令:
- LOAD_VALUE
- ADD_TWO_VALUES
- PRINT_ANSWER
既然我們只關心執行位元組碼的部分,那就不用去管原始碼是如何編譯為上述三個指令的某種排列組合的。我們只要照著編譯後的內容逐指令執行就行了。從另一個方面看,你要是發明了一種新語言,同時編寫了相應的生成位元組碼的編譯器,那就可以在我們的python直譯器上跑了呀。
以 7 + 5 作為原始碼舉例, 編譯後生成以下指令集合:
what_to_execute = {
"instructions": [("LOAD_VALUE", 0), # 第一個數
("LOAD_VALUE", 1), # 第二個數
("ADD_TWO_VALUES", None),
("PRINT_ANSWER", None)],
"numbers": [7, 5] }
在這裡what_to_execute相當於code object, instructions相當於位元組碼。
我們的直譯器是一個堆疊機器,所以是使用棧來完成加法的。首先執行第一個指令 LOAD_VALUE,將第一個數壓入棧中,第二個指令同樣將第二個數壓入棧中。第三個指令 ADD_TWO_VALUES 彈出棧中的兩個數,將它們相加並將結果壓入棧中,最後一個指令彈出棧中的答案並列印。棧的內容變化如下圖所示:
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)
編寫輸入指令集合然後逐指令執行的方法:
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)
執行結果:
儘管我們的直譯器現在還很弱,但它執行指令的過程跟真實Python實際上是差不多的,程式碼裡有幾個需要注意的地方:
- 程式碼中LOAD_VALUE方法的引數是已讀取的常量而不是指令的引數。
- ADD_TWO_VALUES 並不需要任何引數,計算使用的數直接從棧中彈出獲得,這也是基於棧的直譯器的特性。
我們可以利用現有的指令執行3個數甚至多個數的加法:
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_NAME: 讀取變數值,將變數的內容壓棧。
以及新增一個變數名列表。
下面是我們需要執行的指令集合:
#原始碼
def s():
a = 1
b = 2
print(a + b)
#編譯後的位元組碼
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"] }
因為這裡不考慮名稱空間和作用域的問題,所以在實現直譯器的時候可以直接將變數與常量的對映關係以字典的形式儲存在解釋 器物件的成員變數中,同時由於多了變數名列表與操作變數名列表的指令,通過指令引數取得方法引數的時候還需根據指令來判斷所取的是哪一個列表(常量列表還 是變數名列表),因此需要再實現一個解析指令引數的方法。
帶有變數的直譯器的程式碼實現如下:
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 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 parse_argument(self, instruction, argument, what_to_execute):
#解析命令引數
#使用常量列表的方法
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)
執行結果:
相信你已經發現,我們現在才實現五個指令,然而run_code已經看上去有點"腫"了,之後再追加新的指令,它就更"腫"了。不怕,可以利用python的動態方法查詢特性。因為指令名與對應的實現方法名是相同的,所以可以利用getattr方法,getattr會根據輸入的方法名返回對應的方法,這樣就可以擺脫臃腫的分支結構,同時再追加新指令也不用修改原來的run_code程式碼了。
下面是run_code的進化版execute:
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)2
bytecode_method = getattr(self, instruction)
if argument is None:
bytecode_method()
else:
bytecode_method(argument)