814
技術社區[雲棲]
用 Python 實現 Python 解釋器
Allison 是 Dropbox 的工程師,在那裏她維護著這個世界上最大的 Python 客戶端網絡之一。在去 Dropbox 之前,她是 Recurse Center 的協調人, 是這個位於紐約的程序員深造機構的作者。她在北美的 PyCon 做過關於 Python 內部機製的演講,並且她喜歡研究奇怪的 bug。她的博客地址是 akaptur.com。
介紹
Byterun 是一個用 Python 實現的 Python 解釋器。隨著我對 Byterun 的開發,我驚喜地的發現,這個 Python 解釋器的基礎結構用 500 行代碼就能實現。在這一章我們會搞清楚這個解釋器的結構,給你足夠探索下去的背景知識。我們的目標不是向你展示解釋器的每個細節---像編程和計算機科學其他有趣的領域一樣,你可能會投入幾年的時間去深入了解這個主題。
Byterun 是 Ned Batchelder 和我完成的,建立在 Paul Swartz 的工作之上。它的結構和主要的 Python 實現(CPython)差不多,所以理解 Byterun 會幫助你理解大多數解釋器,特別是 CPython 解釋器。(如果你不知道你用的是什麼 Python,那麼很可能它就是 CPython)。盡管 Byterun 很小,但它能執行大多數簡單的 Python 程序(這一章是基於 Python 3.5 及其之前版本生成的字節碼的,在 Python 3.6 中生成的字節碼有一些改變)。
Python 解釋器
在開始之前,讓我們限定一下“Pyhton 解釋器”的意思。在討論 Python 的時候,“解釋器”這個詞可以用在很多不同的地方。有的時候解釋器指的是 Python REPL,即當你在命令行下敲下 python
時所得到的交互式環境。有時候人們會或多或少的互換使用 “Python 解釋器”和“Python”來說明從頭到尾執行 Python 代碼的這一過程。在本章中,“解釋器”有一個更精確的意思:Python 程序的執行過程中的最後一步。
在解釋器接手之前,Python 會執行其他 3 個步驟:詞法分析,語法解析和編譯。這三步合起來把源代碼轉換成代碼對象code object,它包含著解釋器可以理解的指令。而解釋器的工作就是解釋代碼對象中的指令。
你可能很奇怪執行 Python 代碼會有編譯這一步。Python 通常被稱為解釋型語言,就像 Ruby,Perl 一樣,它們和像 C,Rust 這樣的編譯型語言相對。然而,這個術語並不是它看起來的那樣精確。大多數解釋型語言包括 Python 在內,確實會有編譯這一步。而 Python 被稱為解釋型的原因是相對於編譯型語言,它在編譯這一步的工作相對較少(解釋器做相對多的工作)。在這章後麵你會看到,Python 的編譯器比 C 語言編譯器需要更少的關於程序行為的信息。
Python 的 Python 解釋器
Byterun 是一個用 Python 寫的 Python 解釋器,這點可能讓你感到奇怪,但沒有比用 C 語言寫 C 語言編譯器更奇怪的了。(事實上,廣泛使用的 gcc 編譯器就是用 C 語言本身寫的)你可以用幾乎任何語言寫一個 Python 解釋器。
用 Python 寫 Python 既有優點又有缺點。最大的缺點就是速度:用 Byterun 執行代碼要比用 CPython 執行慢的多,CPython 解釋器是用 C 語言實現的,並做了認真優化。然而 Byterun 是為了學習而設計的,所以速度對我們不重要。使用 Python 最大優勢是我們可以僅僅實現解釋器,而不用擔心 Python 運行時部分,特別是對象係統。比如當 Byterun 需要創建一個類時,它就會回退到“真正”的 Python。另外一個優勢是 Byterun 很容易理解,部分原因是它是用人們很容易理解的高級語言寫的(Python !)(另外我們不會對解釋器做優化 —— 再一次,清晰和簡單比速度更重要)
構建一個解釋器
在我們考察 Byterun 代碼之前,我們需要從高層次對解釋器結構有一些了解。Python 解釋器是如何工作的?
Python 解釋器是一個虛擬機virtual machine,是一個模擬真實計算機的軟件。我們這個虛擬機是棧機器stack machine,它用幾個棧來完成操作(與之相對的是寄存器機器register machine,它從特定的內存地址讀寫數據)。
Python 解釋器是一個字節碼解釋器bytecode interpreter:它的輸入是一些稱作字節碼bytecode的指令集。當你寫 Python 代碼時,詞法分析器、語法解析器和編譯器會生成代碼對象code object讓解釋器去操作。每個代碼對象都包含一個要被執行的指令集 —— 它就是字節碼 —— 以及還有一些解釋器需要的信息。字節碼是 Python 代碼的一個中間層表示intermediate representation:它以一種解釋器可以理解的方式來表示源代碼。這和匯編語言作為 C 語言和機器語言的中間表示很類似。
微型解釋器
為了讓說明更具體,讓我們從一個非常小的解釋器開始。它隻能計算兩個數的和,隻能理解三個指令。它執行的所有代碼隻是這三個指令的不同組合。下麵就是這三個指令:
LOAD_VALUE
ADD_TWO_VALUES
PRINT_ANSWER
我們不關心詞法、語法和編譯,所以我們也不在乎這些指令集是如何產生的。你可以想象,當你寫下 7 + 5
,然後一個編譯器為你生成那三個指令的組合。如果你有一個合適的編譯器,你甚至可以用 Lisp 的語法來寫,隻要它能生成相同的指令。
假設
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 解釋器是一個棧機器stack machine,所以它必須通過操作棧來完成這個加法(見下圖)。解釋器先執行第一條指令,LOAD_VALUE
,把第一個數壓到棧中。接著它把第二個數也壓到棧中。然後,第三條指令,ADD_TWO_VALUES
,先把兩個數從棧中彈出,加起來,再把結果壓入棧中。最後一步,把結果彈出並輸出。
棧機器
LOAD_VALUE
這條指令告訴解釋器把一個數壓入棧中,但指令本身並沒有指明這個數是多少。指令需要一個額外的信息告訴解釋器去哪裏找到這個數。所以我們的指令集有兩個部分:指令本身和一個常量列表。(在 Python 中,字節碼就是我們所稱的“指令”,而解釋器“執行”的是代碼對象。)
為什麼不把數字直接嵌入指令之中?想象一下,如果我們加的不是數字,而是字符串。我們可不想把字符串這樣的東西加到指令中,因為它可以有任意的長度。另外,我們這種設計也意味著我們隻需要對象的一份拷貝,比如這個加法 7 + 7
, 現在常量表 "numbers"
隻需包含一個[7]
。
你可能會想為什麼會需要除了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)
這三個方法完成了解釋器所理解的三條指令。但解釋器還需要一樣東西:一個能把所有東西結合在一起並執行的方法。這個方法就叫做 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
不需要任何參數,它從解釋器棧中彈出所需的值。這正是以基於棧的解釋器的特點。
記得我們說過隻要給出合適的指令集,不需要對解釋器做任何改變,我們就能做多個數的加法。考慮下麵的指令集,你覺得會發生什麼?如果你有一個合適的編譯器,什麼代碼才能編譯出下麵的指令集?
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
;和一個變量到值的映射關係。目前,我們會忽略命名空間和作用域,所以我們可以把變量和值的映射直接存儲在解釋器對象中。最後,我們要保證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__
方法中增加一個environment
字典。我們也增加了STORE_NAME
和LOAD_NAME
方法,它們獲得變量名,然後從environment
字典中設置或取出這個變量值。
現在指令的參數就有兩個不同的意思,它可能是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)
僅僅五個指令,run_code
這個方法已經開始變得冗長了。如果保持這種結構,那麼每條指令都需要一個if
分支。這裏,我們要利用 Python 的動態方法查找。我們總會給一個稱為FOO
的指令定義一個名為FOO
的方法,這樣我們就可用 Python 的getattr
函數在運行時動態查找方法,而不用這個大大的分支結構。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 直接訪問這些信息。對於函數對象cond
,cond.__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
)表示對應源代碼的行數。第二列的數字是字節碼的索引,告訴我們指令LOAD_CONST
在位置 0 。第三列是指令本身對應的人類可讀的名字。如果第四列存在,它表示指令的參數。如果第五列存在,它是一個關於參數是什麼的提示。
考慮這個字節碼的前幾個字節:[100, 1, 0, 125, 0, 0]。這 6 個字節表示兩條帶參數的指令。我們可以使用dis.opname
,一個字節到可讀字符串的映射,來找到指令 100 和指令 125 代表的是什麼:
>>> dis.opname[100]
'LOAD_CONST'
>>> dis.opname[125]
'STORE_FAST'
第二和第三個字節 —— 1 、0 ——是LOAD_CONST
的參數,第五和第六個字節 —— 0、0 —— 是STORE_FAST
的參數。就像我們前麵的小例子,LOAD_CONST
需要知道的到哪去找常量,STORE_FAST
需要知道要存儲的名字。(Python 的LOAD_CONST
和我們小例子中的LOAD_VALUE
一樣,LOAD_FAST
和LOAD_NAME
一樣)。所以這六個字節代表第一行源代碼x = 3
(為什麼用兩個字節表示指令的參數?如果 Python 使用一個字節,每個代碼對象你隻能有 256 個常量/名字,而用兩個字節,就增加到了 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
第三行的條件表達式if x < 5
被編譯成四條指令:LOAD_FAST
、 LOAD_CONST
、 COMPARE_OP
和POP_JUMP_IF_FALSE
。x < 5
對應加載x
、加載 5、比較這兩個值。指令POP_JUMP_IF_FALSE
完成這個if
語句。這條指令把棧頂的值彈出,如果值為真,什麼都不發生。如果值為假,解釋器會跳轉到另一條指令。
這條將被加載的指令稱為跳轉目標,它作為指令POP_JUMP
的參數。這裏,跳轉目標是 22,索引為 22 的指令是LOAD_CONST
,對應源碼的第 6 行。(dis
用>>
標記跳轉目標。)如果X < 5
為假,解釋器會忽略第四行(return yes
),直接跳轉到第6行(return "no"
)。因此解釋器通過跳轉指令選擇性的執行指令。
Python 的循環也依賴於跳轉。在下麵的字節碼中,while x < 5
這一行產生了和if x < 10
幾乎一樣的字節碼。在這兩種情況下,解釋器都是先執行比較,然後執行POP_JUMP_IF_FALSE
來控製下一條執行哪個指令。第四行的最後一條字節碼JUMP_ABSOLUT
(循環體結束的地方),讓解釋器返回到循環開始的第 9 條指令處。當x < 10
變為假,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 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
來試試你自己寫的函數。一些有趣的問題值得探索:
- 對解釋器而言 for 循環和 while 循環有什麼不同?
- 能不能寫出兩個不同函數,卻能產生相同的字節碼?
-
elif
是怎麼工作的?列表推導呢?
幀
到目前為止,我們已經知道了 Python 虛擬機是一個棧機器。它能順序執行指令,在指令間跳轉,壓入或彈出棧值。但是這和我們期望的解釋器還有一定距離。在前麵的那個例子中,最後一條指令是RETURN_VALUE
,它和return
語句相對應。但是它返回到哪裏去呢?
為了回答這個問題,我們必須再增加一層複雜性:幀frame。一個幀是一些信息的集合和代碼的執行上下文。幀在 Python 代碼執行時動態地創建和銷毀。每個幀對應函數的一次調用 —— 所以每個幀隻有一個代碼對象與之關聯,而一個代碼對象可以有多個幀。比如你有一個函數遞歸的調用自己 10 次,這會產生 11 個幀,每次調用對應一個,再加上啟動模塊對應的一個幀。總的來說,Python 程序的每個作用域都有一個幀,比如,模塊、函數、類定義。
幀存在於調用棧call stack中,一個和我們之前討論的完全不同的棧。(你最熟悉的棧就是調用棧,就是你經常看到的異常回溯,每個以"File 'program.py'"開始的回溯對應一個幀。)解釋器在執行字節碼時操作的棧,我們叫它數據棧data stack。其實還有第三個棧,叫做塊棧block stack,用於特定的控製流塊,比如循環和異常處理。調用棧中的每個幀都有它自己的數據棧和塊棧。
讓我們用一個具體的例子來說明一下。假設 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
告訴解釋器在幀之間傳遞一個值。首先,它把位於調用棧棧頂的幀中的數據棧的棧頂值彈出。然後把整個幀彈出丟棄。最後把這個值壓到下一個幀的數據棧中。
當 Ned Batchelder 和我在寫 Byterun 時,很長一段時間我們的實現中一直有個重大的錯誤。我們整個虛擬機中隻有一個數據棧,而不是每個幀都有一個。我們寫了很多測試代碼,同時在 Byterun 和真正的 Python 上運行,希望得到一致結果。我們幾乎通過了所有測試,隻有一樣東西不能通過,那就是生成器generators。最後,通過仔細的閱讀 CPython 的源碼,我們發現了錯誤所在(感謝 Michael Arntzenius 對這個 bug 的洞悉)。把數據棧移到每個幀就解決了這個問題。
回頭在看看這個 bug,我驚訝的發現 Python 真的很少依賴於每個幀有一個數據棧這個特性。在 Python 中幾乎所有的操作都會清空數據棧,所以所有的幀公用一個數據棧是沒問題的。在上麵的例子中,當bar
執行完後,它的數據棧為空。即使foo
公用這一個棧,它的值也不會受影響。然而,對應生成器,它的一個關鍵的特點是它能暫停一個幀的執行,返回到其他的幀,一段時間後它能返回到原來的幀,並以它離開時的相同狀態繼續執行。
Byterun
現在我們有足夠的 Python 解釋器的知識背景去考察 Byterun。
Byterun 中有四種對象。
-
VirtualMachine
類,它管理高層結構,尤其是幀調用棧,並包含了指令到操作的映射。這是一個比前麵Inteprter
對象更複雜的版本。 -
Frame
類,每個Frame
類都有一個代碼對象,並且管理著其他一些必要的狀態位,尤其是全局和局部命名空間、指向調用它的整的指針和最後執行的字節碼指令。 -
Function
類,它被用來代替真正的 Python 函數。回想一下,調用函數時會創建一個新的幀。我們自己實現了Function
,以便我們控製新的Frame
的創建。 -
Block
類,它隻是包裝了塊的 3 個屬性。(塊的細節不是解釋器的核心,我們不會花時間在它身上,把它列在這裏,是因為 Byterun 需要它。)
VirtualMachine
類
每次程序運行時隻會創建一個VirtualMachine
實例,因為我們隻有一個 Python 解釋器。VirtualMachine
保存調用棧、異常狀態、在幀之間傳遞的返回值。它的入口點是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)
Frame
類
接下來,我們來寫Frame
對象。幀是一個屬性的集合,它沒有任何方法。前麵提到過,這些屬性包括由編譯器生成的代碼對象;局部、全局和內置命名空間;前一個幀的引用;一個數據棧;一個塊棧;最後執行的指令指針。(對於內置命名空間我們需要多做一點工作,Python 在不同模塊中對這個命名空間有不同的處理;但這個細節對我們的虛擬機不重要。)
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):
[... 刪節 ...]
# 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__
方法 —— 它創建一個新的Frame
並運行它。
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', <
最後更新:2017-06-07 17:03:43
上一篇:
如何在 CentOS 7 用 cPanel 配置 Nginx 反向代理
下一篇:
科學音頻處理(一):怎樣使用 Octave 對音頻文件進行讀寫操作