Byterun 是一個用 Python 實現的 Python 解釋器。隨着我對 Byterun 的開發,我驚喜地的發現,這個 Python 解釋器的基礎結構用 500 行代碼就能實現。在這一章咱們會搞清楚這個解釋器的結構,給你足夠探索下去的背景知識。咱們的目標不是向你展現解釋器的每一個細節—像編程和計算機科學其餘有趣的領域同樣,你可能會投入幾年的時間去深刻了解這個主題。python
Byterun 是 Ned Batchelder 和我完成的,創建在 Paul Swartz 的工做之上。它的結構和主要的 Python 實現(CPython)差很少,因此理解 Byterun 會幫助你理解大多數解釋器,特別是 CPython 解釋器。(若是你不知道你用的是什麼 Python,那麼極可能它就是 CPython)。儘管 Byterun 很小,但它能執行大多數簡單的 Python 程序(這一章是基於 Python 3.5 及其以前版本生成的字節碼的,在 Python 3.6 中生成的字節碼有一些改變)。git
在開始以前,讓咱們限定一下「Pyhton 解釋器」的意思。在討論 Python 的時候,「解釋器」這個詞能夠用在不少不一樣的地方。有的時候解釋器指的是 Python REPL,即當你在命令行下敲下 python
時所獲得的交互式環境。有時候人們會或多或少的互換使用 「Python 解釋器」和「Python」來講明從頭至尾執行 Python 代碼的這一過程。在本章中,「解釋器」有一個更精確的意思:Python 程序的執行過程當中的最後一步。程序員
在解釋器接手以前,Python 會執行其餘 3 個步驟:詞法分析,語法解析和編譯。這三步合起來把源代碼轉換成代碼對象(code object),它包含着解釋器能夠理解的指令。而解釋器的工做就是解釋代碼對象中的指令。github
你可能很奇怪執行 Python 代碼會有編譯這一步。Python 一般被稱爲解釋型語言,就像 Ruby,Perl 同樣,它們和像 C,Rust 這樣的編譯型語言相對。然而,這個術語並非它看起來的那樣精確。大多數解釋型語言包括 Python 在內,確實會有編譯這一步。而 Python 被稱爲解釋型的緣由是相對於編譯型語言,它在編譯這一步的工做相對較少(解釋器作相對多的工做)。在這章後面你會看到,Python 的編譯器比 C 語言編譯器須要更少的關於程序行爲的信息。編程
Byterun 是一個用 Python 寫的 Python 解釋器,這點可能讓你感到奇怪,但沒有比用 C 語言寫 C 語言編譯器更奇怪的了。(事實上,普遍使用的 gcc 編譯器就是用 C 語言自己寫的)你能夠用幾乎任何語言寫一個 Python 解釋器。網絡
用 Python 寫 Python 既有優勢又有缺點。最大的缺點就是速度:用 Byterun 執行代碼要比用 CPython 執行慢的多,CPython 解釋器是用 C 語言實現的,並作了認真優化。然而 Byterun 是爲了學習而設計的,因此速度對咱們不重要。使用 Python 最大優點是咱們能夠僅僅實現解釋器,而不用擔憂 Python 運行時部分,特別是對象系統。好比當 Byterun 須要建立一個類時,它就會回退到「真正」的 Python。另一個優點是 Byterun 很容易理解,部分緣由是它是用人們很容易理解的高級語言寫的(Python !)(另外咱們不會對解釋器作優化 —— 再一次,清晰和簡單比速度更重要)app
在咱們考察 Byterun 代碼以前,咱們須要從高層次對解釋器結構有一些瞭解。Python 解釋器是如何工做的?ide
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 的語法來寫,只要它能生成相同的指令。
假設
1
2
|
7
+
5
|
生成這樣的指令集:
1
2
3
4
5
6
7
|
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
會把一個值壓入棧中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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
做爲參數,循環執行裏面的每條指令,若是指令有參數就處理參數,而後調用解釋器對象中相應的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
。
1
2
3
|
interpreter
=
Interpreter
(
)
interpreter
.
run_code
(
what_to_execute
)
|
顯然,它會輸出 12。
儘管咱們的解釋器功能十分受限,但這個過程幾乎和真正的 Python 解釋器處理加法是同樣的。這裏,咱們還有幾點要注意。
首先,一些指令須要參數。在真正的 Python 字節碼當中,大概有一半的指令有參數。像咱們的例子同樣,參數和指令打包在一塊兒。注意指令的參數和傳遞給對應方法的參數是不一樣的。
第二,指令ADD_TWO_VALUES
不須要任何參數,它從解釋器棧中彈出所需的值。這正是以基於棧的解釋器的特色。
記得咱們說過只要給出合適的指令集,不須要對解釋器作任何改變,咱們就能作多個數的加法。考慮下面的指令集,你以爲會發生什麼?若是你有一個合適的編譯器,什麼代碼才能編譯出下面的指令集?
1
2
3
4
5
6
7
8
9
|
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
除了一個常量列表,還要有個變量名字的列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
>>>
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
列表的索引。解釋器經過檢查所執行的指令就能知道是那種參數。而咱們打破這種邏輯 ,把指令和它所用何種參數的映射關係放在另外一個單獨的方法中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
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
方法如今是這樣:
1
2
3
4
5
6
7
8
9
10
11
|
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 字節碼。字節碼的結構和咱們的小解釋器的指令集差很少,除了字節碼用一個字節而不是一個名字來表明這條指令。爲了理解它的結構,咱們將考察一個函數的字節碼。考慮下面這個例子:
1
2
3
4
5
6
7
|
>>>
def
cond
(
)
:
.
.
.
x
=
3
.
.
.
if
x
<
5
:
.
.
.
return
'yes'
.
.
.
else
:
.
.
.
return
'no'
.
.
.
|
Python 在運行時會暴露一大批內部信息,而且咱們能夠經過 REPL 直接訪問這些信息。對於函數對象cond
,cond.__code__
是與其關聯的代碼對象,而cond.__code__.co_code
就是它的字節碼。當你寫 Python 代碼時,你永遠也不會想直接使用這些屬性,可是這可讓咱們作出各類惡做劇,同時也能夠看看內部機制。
1
2
3
4
5
6
|
>>>
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
,它輸出每一個字節碼的解釋。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
>>>
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 表明的是什麼:
1
2
3
4
5
|
>>>
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
函數的反彙編結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
>>>
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 被編譯成四條指令:
LOAD_FAST
、 LOAD_CONST
、 COMPARE_OP
和 POP_JUMP_IF_FALSE
。x 對應加載
x
、加載 五、比較這兩個值。指令POP_JUMP_IF_FALSE
完成這個if
語句。這條指令把棧頂的值彈出,若是值爲真,什麼都不發生。若是值爲假,解釋器會跳轉到另外一條指令。
這條將被加載的指令稱爲跳轉目標,它做爲指令POP_JUMP
的參數。這裏,跳轉目標是 22,索引爲 22 的指令是LOAD_CONST
,對應源碼的第 6 行。(dis
用>>
標記跳轉目標。)若是X 爲假,解釋器會忽略第四行(
return yes
),直接跳轉到第6行(return "no"
)。所以解釋器經過跳轉指令選擇性的執行指令。
Python 的循環也依賴於跳轉。在下面的字節碼中,while x 這一行產生了和
if x 幾乎同樣的字節碼。在這兩種狀況下,解釋器都是先執行比較,而後執行
POP_JUMP_IF_FALSE
來控制下一條執行哪一個指令。第四行的最後一條字節碼JUMP_ABSOLUT
(循環體結束的地方),讓解釋器返回到循環開始的第 9 條指令處。當 x 變爲假,
POP_JUMP_IF_FALSE
會讓解釋器跳到循環的終止處,第 34 條指令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
>>>
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
來試試你本身寫的函數。一些有趣的問題值得探索:
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
。
1
2
3
4
5
6
7
8
9
10
11
|
>>>
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
公用這一個棧,它的值也不會受影響。然而,對應生成器,它的一個關鍵的特色是它能暫停一個幀的執行,返回到其餘的幀,一段時間後它能返回到原來的幀,並以它離開時的相同狀態繼續執行。
如今咱們有足夠的 Python 解釋器的知識背景去考察 Byterun。
Byterun 中有四種對象。
VirtualMachine
類,它管理高層結構,尤爲是幀調用棧,幷包含了指令到操做的映射。這是一個比前面Inteprter
對象更復雜的版本。Frame
類,每一個Frame
類都有一個代碼對象,而且管理着其餘一些必要的狀態位,尤爲是全局和局部命名空間、指向調用它的整的指針和最後執行的字節碼指令。Function
類,它被用來代替真正的 Python 函數。回想一下,調用函數時會建立一個新的幀。咱們本身實現了Function
,以便咱們控制新的Frame
的建立。Block
類,它只是包裝了塊的 3 個屬性。(塊的細節不是解釋器的核心,咱們不會花時間在它身上,把它列在這裏,是由於 Byterun 須要它。)VirtualMachine
類每次程序運行時只會建立一個VirtualMachine
實例,由於咱們只有一個 Python 解釋器。VirtualMachine
保存調用棧、異常狀態、在幀之間傳遞的返回值。它的入口點是run_code
方法,它以編譯後的代碼對象爲參數,以建立一個幀爲開始,而後運行這個幀。這個幀可能再建立出新的幀;調用棧隨着程序的運行而增加和縮短。當第一個幀返回時,執行結束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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 在不一樣模塊中對這個命名空間有不一樣的處理;但這個細節對咱們的虛擬機不重要。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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
,完成執行幀的主要工做,待會咱們再討論這個方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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
並運行它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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
以及其餘操做棧的指令的實現可讀性更高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class
VirtualMachine
(
object
)
:
[
.
.
.
刪節
.
.
.
]
# 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
[
]
|
在咱們運行幀以前,咱們還需兩個方法。
第一個方法,parse_byte_and_args
以一個字節碼爲輸入,先檢查它是否有參數,若是有,就解析它的參數。這個方法同時也更新幀的last_instruction
屬性,它指向最後執行的指令。一條沒有參數的指令只有一個字節長度,而有參數的字節有3個字節長。參數的意義依賴於指令是什麼。好比,前面說過,指令POP_JUMP_IF_FALSE
,它的參數指的是跳轉目標。BUILD_LIST
,它的參數是列表的個數。LOAD_CONST
,它的參數是常量的索引。
一些指令用簡單的數字做爲參數。對於另外一些,虛擬機須要一點努力去發現它含意。標準庫中的dis
模塊中有一個備忘單,它解釋什麼參數有什麼意思,這讓咱們的代碼更加簡潔。好比,列表dis.hasname
告訴咱們LOAD_NAME
、 IMPORT_NAME
、LOAD_GLOBAL
,以及另外的 9 個指令的參數都有一樣的意義:對於這些指令,它們的參數表明了代碼對象中的名字列表的索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class
VirtualMachine
(
object
)
:
[
.
.
.
刪節
.
.
.
]
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 中,這個分派函數用一個巨大的 switch 語句實現,有超過 1500 行的代碼。幸運的是,咱們用的是 Python,咱們的代碼會簡潔的多。咱們會爲每個字節碼名字定義一個方法,而後用getattr
來查找。就像咱們前面的小解釋器同樣,若是一條指令叫作FOO_BAR
,那麼它對應的方法就是byte_FOO_BAR
。如今,咱們先把這些方法當作一個黑盒子。每一個指令方法都會返回None
或者一個字符串why
,有些狀況下虛擬機須要這個額外why
信息。這些指令方法的返回值,僅做爲解釋器狀態的內部指示,千萬不要和執行幀的返回值相混淆。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
class
VirtualMachine
(
object
)
:
[
.
.
.
刪節
.
.
.
]
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
|
Block
類在咱們完成每一個字節碼方法前,咱們簡單的討論一下塊。一個塊被用於某種控制流,特別是異常處理和循環。它負責保證當操做完成後數據棧處於正確的狀態。好比,在一個循環中,一個特殊的迭代器會存在棧中,當循環完成時它從棧中彈出。解釋器須要檢查循環仍在繼續仍是已經中止。
爲了跟蹤這些額外的信息,解釋器設置了一個標誌來指示它的狀態。咱們用一個變量why
實現這個標誌,它能夠是None
或者是下面幾個字符串之一:"continue"
、"break"
、"excption"
、return
。它們指示對塊棧和數據棧進行什麼操做。回到咱們迭代器的例子,若是塊棧的棧頂是一個loop
塊,why
的代碼是continue
,迭代器就應該保存在數據棧上,而若是why
是break
,迭代器就會被彈出。
塊操做的細節比這個還要繁瑣,咱們不會花時間在這上面,可是有興趣的讀者值得仔細的看看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
Block
=
collections
.
namedtuple
(
"Block"
,
"type, handler, stack_height"
)
class
VirtualMachine
(
object
)
:
[
.
.
.
刪節
.
.
.
]
# Block stack manipulation
def
push_block
(
self
,
b_type
,
handler
=
None
)
:
level
=
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
|
剩下了的就是完成那些指令方法了:byte_LOAD_FAST
、byte_BINARY_MODULO
等等。而這些指令的實現並非頗有趣,這裏咱們只展現了一小部分,完整的實現在 GitHub 上。(這裏包括的指令足夠執行咱們前面所述的全部代碼了。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
|
class
VirtualMachine
(
object
)
:
[
.
.
.
刪節
.
.
.
]
## 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
]
&n
|