用Python實(shí)現(xiàn)一個(gè)Python解釋器

程派微信號(hào):codingpy

本文約有 15000 字钾埂,讀完可能需要 20 分鐘愉粤。

原文地址: 500lines。譯者:qingyunha亡脸。

Allison是Dropbox的工程師,在那里她維護(hù)著世界上最大的由Python客戶組成的網(wǎng)絡(luò)树酪。在Dropbox之前浅碾,她是Recurse Center的引導(dǎo)師, … 她在北美的PyCon做過關(guān)于Python內(nèi)部機(jī)制的演講,并且她喜歡奇怪的bugs续语。她的博客地址是akaptur.com.

Introduction

Byterun是一個(gè)用Python實(shí)現(xiàn)的Python解釋器垂谢。隨著我在Byterun上的工作,我驚訝并很高興地的發(fā)現(xiàn)疮茄,這個(gè)Python解釋器的基礎(chǔ)結(jié)構(gòu)可以滿足500行的限制滥朱。在這一章我們會(huì)搞清楚這個(gè)解釋器的結(jié)構(gòu)根暑,給你足夠的知識(shí)探索下去。我們的目標(biāo)不是向你展示解釋器的每個(gè)細(xì)節(jié)-—像編程和計(jì)算機(jī)科學(xué)其他有趣的領(lǐng)域一樣徙邻,你可能會(huì)投入幾年的時(shí)間去搞清楚這個(gè)主題排嫌。

Byterun是Ned Batchelder和我完成的,建立在Paul Swartz的工作之上缰犁。它的結(jié)構(gòu)和主要的Python實(shí)現(xiàn)(CPython)差不多淳地,所以理解Byterun會(huì)幫助你理解大多數(shù)解釋器特別是CPython解釋器。(如果你不知道你用的是什么Python帅容,那么很可能它就是CPython)颇象。盡管Byterun很小,但它能執(zhí)行大多數(shù)簡(jiǎn)單的Python程序并徘。

A Python Interpreter

在開始之前遣钳,讓我們縮小一下”Pyhton解釋器”的意思。在討論P(yáng)ython的時(shí)候饮亏,”解釋器”這個(gè)詞可以用在很多不同的地方耍贾。有的時(shí)候解釋器指的是REPL,當(dāng)你在命令行下敲下python時(shí)所得到的交互式環(huán)境路幸。有時(shí)候人們會(huì)相互替代的使用Python解釋器和Python來說明執(zhí)行Python代碼的這一過程。在本章付翁,”解釋器”有一個(gè)更精確的意思:執(zhí)行Python程序過程中的最后一步简肴。

在解釋器接手之前,Python會(huì)執(zhí)行其他3個(gè)步驟:詞法分析百侧,語法解析和編譯砰识。這三步合起來把源代碼轉(zhuǎn)換成code object,它包含著解釋器可以理解的指令。而解釋器的工作就是解釋code object中的指令佣渴。

你可能很奇怪執(zhí)行Python代碼會(huì)有編譯這一步辫狼。Python通常被稱為解釋型語言,就像Ruby辛润,Perl一樣膨处,它們和編譯型語言相對(duì),比如C砂竖,Rust真椿。然而,這里的術(shù)語并不是它看起來的那樣精確乎澄。大多數(shù)解釋型語言包括Python突硝,確實(shí)會(huì)有編譯這一步。而Python被稱為解釋型的原因是相對(duì)于編譯型語言置济,它在編譯這一步的工作相對(duì)較少(解釋器做相對(duì)多的工作)解恰。在這章后面你會(huì)看到锋八,Python的編譯器比C語言編譯器需要更少的關(guān)于程序行為的信息。

A Python Python Interpreter

Byterun是一個(gè)用Python寫的Python解釋器护盈,這點(diǎn)可能讓你感到奇怪查库,但沒有比用C語言寫C語言編譯器更奇怪。(事實(shí)上黄琼,廣泛使用的gcc編譯器就是用C語言本身寫的)你可以用幾乎的任何語言寫一個(gè)Python解釋器樊销。

用Python寫Python既有優(yōu)點(diǎn)又有缺點(diǎn)。最大的缺點(diǎn)就是速度:用Byterun執(zhí)行代碼要比用CPython執(zhí)行慢的多脏款,CPython解釋器是用C語言實(shí)現(xiàn)的并做了優(yōu)化围苫。然而Byterun是為了學(xué)習(xí)而設(shè)計(jì)的,所以速度對(duì)我們不重要撤师。使用Python最大優(yōu)點(diǎn)是我們可以僅僅實(shí)現(xiàn)解釋器剂府,而不用擔(dān)心Python運(yùn)行時(shí)的部分,特別是對(duì)象系統(tǒng)剃盾。比如當(dāng)Byterun需要?jiǎng)?chuàng)建一個(gè)類時(shí)腺占,它就會(huì)回退到”真正”的Python。另外一個(gè)優(yōu)勢(shì)是Byterun很容易理解痒谴,部分原因是它是用高級(jí)語言寫的(PythonKゲ)(另外我們不會(huì)對(duì)解釋器做優(yōu)化
-— 再一次,清晰和簡(jiǎn)單比速度更重要)

Building an Interpreter

在我們考察Byterun代碼之前积蔚,我們需要一些對(duì)解釋器結(jié)構(gòu)的高層次視角意鲸。Python解釋器是如何工作的?

Python解釋器是一個(gè)虛擬機(jī),模擬真實(shí)計(jì)算機(jī)的軟件尽爆。我們這個(gè)虛擬機(jī)是棧機(jī)器怎顾,它用幾個(gè)棧來完成操作(與之相對(duì)的是寄存器機(jī)器,它從特定的內(nèi)存地址讀寫數(shù)據(jù))漱贱。

Python解釋器是一個(gè)字節(jié)碼解釋器:它的輸入是一些命令集合稱作字節(jié)碼槐雾。當(dāng)你寫Python代碼時(shí),詞法分析器幅狮,語法解析器和編譯器生成code object讓解釋器去操作募强。每個(gè)code object都包含一個(gè)要被執(zhí)行的指令集合 -— 它就是字節(jié)碼 -—
另外還有一些解釋器需要的信息。字節(jié)碼是Python代碼的一個(gè)中間層表示:它以一種解釋器可以理解的方式來表示源代碼彪笼。這和匯編語言作為C語言和機(jī)器語言的中間表示很類似钻注。

A Tiny Interpreter

為了讓說明更具體,讓我們從一個(gè)非常小的解釋器開始配猫。它只能計(jì)算兩個(gè)數(shù)的和幅恋,只能理解三個(gè)指令。它執(zhí)行的所有代碼只是這三個(gè)指令的不同組合泵肄。下面就是這三個(gè)指令:

  • LOAD_VALUE

  • ADD_TWO_VALUES

  • PRINT_ANSWER

我們不關(guān)心詞法捆交,語法和編譯淑翼,所以我們也不在乎這些指令是如何產(chǎn)生的。你可以想象品追,你寫下7 + 5玄括,然后一個(gè)編譯器為你生成那三個(gè)指令的組合。如果你有一個(gè)合適的編譯器肉瓦,你甚至可以用Lisp的語法來寫遭京,只要它能生成相同的指令。

假設(shè)

生成這樣的指令集:

Python解釋器是一個(gè)棧機(jī)器泞莉,所以它必須通過操作棧來完成這個(gè)加法哪雕。(Figure
1.1)解釋器先執(zhí)行第一條指令,LOAD_VALUE鲫趁,把第一個(gè)數(shù)壓到棧中斯嚎。接著它把第二個(gè)數(shù)也壓到棧中。然后挨厚,第三條指令堡僻,ADD_TWO_VALUES,先把兩個(gè)數(shù)從棧中彈出,加起來疫剃,再把結(jié)果壓入棧中钉疫。最后一步,把結(jié)果彈出并輸出慌申。

LOAD_VALUE這條指令告訴解釋器把一個(gè)數(shù)壓入棧中陌选,但指令本身并沒有指明這個(gè)數(shù)是多少。指令需要一個(gè)額外的信息告訴解釋器去哪里找到這個(gè)數(shù)蹄溉。所以我們的指令集有兩個(gè)部分:指令本身和一個(gè)常量列表。(在Python中您炉,字節(jié)碼就是我們稱為的”指令”柒爵,而解釋器執(zhí)行的是code object。)

為什么不把數(shù)字直接嵌入指令之中赚爵?想象一下棉胀,如果我們加的不是數(shù)字,而是字符串冀膝。我們可不想把字符串這樣的東西加到指令中唁奢,因?yàn)樗梢杂腥我獾拈L(zhǎng)度。另外窝剖,我們這種設(shè)計(jì)也意味著我們只需要對(duì)象的一份拷貝麻掸,比如這個(gè)加法7 + 7, 現(xiàn)在常量表 "numbers"只需包含一個(gè)7

你可能會(huì)想為什么會(huì)需要除了ADD_TWO_VALUES之外的指令赐纱。的確脊奋,對(duì)于我們兩個(gè)數(shù)加法熬北,這個(gè)例子是有點(diǎn)人為制作的意思。然而诚隙,這個(gè)指令卻是建造更復(fù)雜程序的輪子讶隐。比如,就我們目前定義的三個(gè)指令久又,只要給出正確的指令組合巫延,我們可以做三個(gè)數(shù)的加法,或者任意個(gè)數(shù)的加法地消。同時(shí)炉峰,棧提供了一個(gè)清晰的方法去跟蹤解釋器的狀態(tài),這為我們?cè)鲩L(zhǎng)的復(fù)雜性提供了支持犯建。

現(xiàn)在讓我們來完成我們的解釋器讲冠。解釋器對(duì)象需要一個(gè)棧,它可以用一個(gè)列表來表示适瓦。它還需要一個(gè)方法來描述怎樣執(zhí)行每條指令竿开。比如,LOAD_VALUE會(huì)把一個(gè)值壓入棧中玻熙。

這三個(gè)方法完成了解釋器所理解的三條指令否彩。但解釋器還需要一樣?xùn)|西:一個(gè)能把所有東西結(jié)合在一起并執(zhí)行的方法。這個(gè)方法就叫做run_code, 它把我們前面定義的字典結(jié)構(gòu)what-to-execute作為參數(shù)嗦随,循環(huán)執(zhí)行里面的每條指令列荔,如何指令有參數(shù),處理參數(shù)枚尼,然后調(diào)用解釋器對(duì)象中相應(yīng)的方法贴浙。

為了測(cè)試,我們創(chuàng)建一個(gè)解釋器對(duì)象署恍,然后用前面定義的 7 + 5 的指令集來調(diào)用run_code崎溃。

顯然,它會(huì)輸出12盯质。

盡管我們的解釋器功能受限袁串,但這個(gè)加法過程幾乎和真正的Python解釋器是一樣的。這里呼巷,我們還有幾點(diǎn)要注意囱修。

首先,一些指令需要參數(shù)王悍。在真正的Python bytecode中破镰,大概有一半的指令有參數(shù)。像我們的例子一樣,參數(shù)和指令打包在一起啤咽。注意指令的參數(shù)和傳遞給對(duì)應(yīng)方法的參數(shù)是不同的晋辆。

第二,指令ADD_TWO_VALUES不需要任何參數(shù)宇整,它從解釋器棧中彈出所需的值瓶佳。這正是以棧為基礎(chǔ)的解釋器的特點(diǎn)。

記得我們說過只要給出合適的指令集鳞青,不需要對(duì)解釋器做任何改變霸饲,我們做多個(gè)數(shù)的加法”弁兀考慮下面的指令集厚脉,你覺得會(huì)發(fā)生什么?如果你有一個(gè)合適的編譯器胶惰,什么代碼才能編譯出下面的指令集傻工?

從這點(diǎn)出發(fā),我們開始看到這種結(jié)構(gòu)的可擴(kuò)展性:我們可以通過向解釋器對(duì)象增加方法來描述更多的操作(只要有一個(gè)編譯器能為我們生成組織良好的指令集)孵滞。

Variables

接下來給我們的解釋器增加變量的支持中捆。我們需要一個(gè)保存變量值的指令,STORE_NAME;一個(gè)取變量值的指令LOAD_NAME;和一個(gè)變量到值的映射關(guān)系坊饶。目前泄伪,我們會(huì)忽略命名空間和作用域,所以我們可以把變量和值的映射直接存儲(chǔ)在解釋器對(duì)象中匿级。最后蟋滴,我們要保證what_to_execute除了一個(gè)常量列表,還要有個(gè)變量名字的列表痘绎。

我們的新的的實(shí)現(xiàn)在下面津函。為了跟蹤哪名字綁定到那個(gè)值,我們?cè)?code>__init__方法中增加一個(gè)environment字典孤页。我們也增加了STORE_NAMELOAD_NAME方法球散,它們獲得變量名,然后從environment字典中設(shè)置或取出這個(gè)變量值散庶。

現(xiàn)在指令參數(shù)就有兩個(gè)不同的意思,它可能是numbers列表的索引凌净,也可能是names列表的索引悲龟。解釋器通過檢查所執(zhí)行的指令就能知道是那種參數(shù)。而我們打破這種邏輯冰寻,把指令和它所用何種參數(shù)的映射關(guān)系放在另一個(gè)單獨(dú)的方法中须教。

僅僅五個(gè)指令,run_code這個(gè)方法已經(jīng)開始變得冗長(zhǎng)了。如果保持這種結(jié)構(gòu)轻腺,那么每條指令都需要一個(gè)if分支乐疆。這里,我們要利用Python的動(dòng)態(tài)方法查找贬养。我們總會(huì)給一個(gè)稱為FOO的指令定義一個(gè)名為FOO的方法挤土,這樣我們就可用Python的getattr函數(shù)在運(yùn)行時(shí)動(dòng)態(tài)查找方法,而不用這個(gè)大大的分支結(jié)構(gòu)误算。run_code方法現(xiàn)在是這樣:

Real Python Bytecode

現(xiàn)在仰美,放棄我們的小指令集,去看看真正的Python字節(jié)碼儿礼。字節(jié)碼的結(jié)構(gòu)和我們的小解釋器的指令集差不多咖杂,除了字節(jié)碼用一個(gè)字節(jié)而不是一個(gè)名字來指示這條指令。為了理解它的結(jié)構(gòu)蚊夫,我們將考察一個(gè)函數(shù)的字節(jié)碼诉字。考慮下面這個(gè)例子:

Python在運(yùn)行時(shí)會(huì)暴露一大批內(nèi)部信息知纷,并且我們可以通過REPL直接訪問這些信息壤圃。對(duì)于函數(shù)對(duì)象condcond.__code__是與其關(guān)聯(lián)的code object屈扎,而cond.__code__.co_code就是它的字節(jié)碼埃唯。當(dāng)你寫Python代碼時(shí),你永遠(yuǎn)也不會(huì)想直接使用這些屬性鹰晨,但是這可以讓我們做出各種惡作劇墨叛,同時(shí)也可以看看內(nèi)部機(jī)制。

當(dāng)我們直接輸出這個(gè)字節(jié)碼模蜡,它看起來完全無法理解 -—
唯一我們了解的是它是一串字節(jié)漠趁。很幸運(yùn),我們有一個(gè)很強(qiáng)大的工具可以用:Python標(biāo)準(zhǔn)庫中的dis module忍疾。

dis是一個(gè)字節(jié)碼反匯編器闯传。反匯編器以為機(jī)器而寫的底層代碼作為輸入,比如匯編代碼和字節(jié)碼卤妒,然后以人類可讀的方式輸出甥绿。當(dāng)我們運(yùn)行dis.dis, 它輸出每個(gè)字節(jié)碼的解釋。

這些都是什么意思则披?讓我們以第一條指令LOAD_CONST為例子共缕。第一列的數(shù)字(2)表示對(duì)應(yīng)源代碼的行數(shù)。第二列的數(shù)字是字節(jié)碼的索引士复,告訴我們指令LOAD_CONST在0位置图谷。第三列是指令本身對(duì)應(yīng)的人類可讀的名字翩活。如果第四列存在,它表示指令的參數(shù)便贵。如果第5列存在盗誊,它是一個(gè)關(guān)于參數(shù)是什么的提示看峻。

考慮這個(gè)字節(jié)碼的前幾個(gè)字節(jié):[100, 1, 0, 125, 0, 0]覆致。這6個(gè)字節(jié)表示兩條帶參數(shù)的指令肠骆。我們可以使用dis.opname,一個(gè)字節(jié)到可讀字符串的映射绸硕,來找到指令100和指令125代表是什么:

第二和第三個(gè)字節(jié) -— 1 堂竟,0 —-是LOAD_CONST的參數(shù),第五和第六個(gè)字節(jié) -— 0玻佩,0 —-
STORE_FAST的參數(shù)出嘹。就像我們前面的小例子,LOAD_CONST需要知道的到哪去找常量咬崔,STORE_FAST需要找到名字税稼。(Python的LOAD_CONST和我們小例子中的LOAD_VALUE一樣,LOAD_FASTLOAD_NAME一樣)垮斯。所以這六個(gè)字節(jié)代表第一行源代碼x = 3.(為什么用兩個(gè)字節(jié)表示指令的參數(shù)郎仆?如果Python使用一個(gè)字節(jié),每個(gè)code object你只能有256個(gè)常量/名字兜蠕,而用兩個(gè)字節(jié)扰肌,就增加到了256的平方,65536個(gè))熊杨。

Conditionals and Loops

到目前為止曙旭,我們的解釋器只能一條接著一條的執(zhí)行指令。這有個(gè)問題晶府,我們經(jīng)常會(huì)想多次執(zhí)行某個(gè)指令桂躏,或者在特定的條件下略過它們。為了可以寫循環(huán)和分支結(jié)構(gòu)川陆,解釋器必須能夠在指令中跳轉(zhuǎn)剂习。在某種程度上,Python在字節(jié)碼中使用GOTO語句來處理循環(huán)和分支较沪!讓我們?cè)倏匆粋€(gè)cond函數(shù)的反匯編結(jié)果:

第三行的條件表達(dá)式if x < 5被編譯成四條指令:LOAD_FAST, LOAD_CONST, COMPARE_OPPOP_JUMP_IF_FALSE鳞绕。x < 5對(duì)應(yīng)加載x,加載5尸曼,比較這兩個(gè)值猾昆。指令POP_JUMP_IF_FALSE完成if語句。這條指令把棧頂?shù)闹祻棾雎獍绻禐檎妫裁炊疾话l(fā)生。如果值為假解幽,解釋器會(huì)跳轉(zhuǎn)到另一條指令贴见。

這條將被加載的指令稱為跳轉(zhuǎn)目標(biāo),它作為指令POP_JUMP的參數(shù)躲株。這里片部,跳轉(zhuǎn)目標(biāo)是22,索引為22的指令是LOAD_CONST,對(duì)應(yīng)源碼的第6行霜定。(dis>>標(biāo)記跳轉(zhuǎn)目標(biāo)档悠。)如果X < 5為假,解釋器會(huì)忽略第四行(return yes),直接跳轉(zhuǎn)到第6行(return "no")望浩。因此解釋器通過跳轉(zhuǎn)指令選擇性的執(zhí)行指令辖所。

Python的循環(huán)也依賴于跳轉(zhuǎn)。在下面的字節(jié)碼中磨德,while x < 5這一行產(chǎn)生了和if x < 10幾乎一樣的字節(jié)碼缘回。在這兩種情況下,解釋器都是先執(zhí)行比較典挑,然后執(zhí)行POP_JUMP_IF_FALSE來控制下一條執(zhí)行哪個(gè)指令酥宴。第四行的最后一條字節(jié)碼JUMP_ABSOLUT(循環(huán)體結(jié)束的地方),讓解釋器返回到循環(huán)開始的第9條指令處您觉。當(dāng)x < 10變?yōu)榧伲?code>POP_JUMP_IF_FALSE會(huì)讓解釋器跳到循環(huán)的終止處拙寡,第34條指令。

Explore Bytecode

我鼓勵(lì)你用dis.dis來試試你自己寫的函數(shù)琳水。一些有趣的問題值得探索:

  • 對(duì)解釋器而言for循環(huán)和while循環(huán)有什么不同肆糕?

  • 能不能寫出兩個(gè)不同函數(shù),卻能產(chǎn)生相同的字節(jié)碼?

  • elif是怎么工作的炫刷?列表推導(dǎo)呢擎宝?

Frames

到目前為止,我們已經(jīng)知道了Python虛擬機(jī)是一個(gè)棧機(jī)器浑玛。它能順序執(zhí)行指令绍申,在指令間跳轉(zhuǎn),壓入或彈出棧值顾彰。但是這和我們心想的解釋器還有一定距離极阅。在前面的那個(gè)例子中,最后一條指令是RETURN_VALUE,它和return語句想對(duì)應(yīng)涨享。但是它返回到哪里去呢筋搏?

為了回答這個(gè)問題,我們必須嚴(yán)增加一層復(fù)雜性:frame厕隧。一個(gè)frame是一些信息的集合和代碼的執(zhí)行上下文奔脐。frames在Python代碼執(zhí)行時(shí)動(dòng)態(tài)的創(chuàng)建和銷毀俄周。每個(gè)frame對(duì)應(yīng)函數(shù)的一次調(diào)用。-— 所以每個(gè)frame只有一個(gè)code object與之關(guān)聯(lián)髓迎,而一個(gè)code object可以很多frame峦朗。比如你有一個(gè)函數(shù)遞歸的調(diào)用自己10次,這時(shí)有11個(gè)frame排龄〔ㄊ疲總的來說,Python程序的每個(gè)作用域有一個(gè)frame橄维,比如尺铣,每個(gè)module,每個(gè)函數(shù)調(diào)用争舞,每個(gè)類定義凛忿。

Frame存在于調(diào)用棧中,一個(gè)和我們之前討論的完全不同的棧兑障。(你最熟悉的棧就是調(diào)用棧侄非,就是你經(jīng)常看到的異沉饕耄回溯逞怨,每個(gè)以”File ‘program.py’”開始的回溯對(duì)應(yīng)一個(gè)frame。)解釋器在執(zhí)行字節(jié)碼時(shí)操作的棧福澡,我們叫它數(shù)據(jù)棧叠赦。其實(shí)還有第三個(gè)棧,叫做塊棧革砸,用于特定的控制流塊除秀,比如循環(huán)和異常處理。調(diào)用棧中的每個(gè)frame都有它自己的數(shù)據(jù)棧和塊棧算利。

讓我們用一個(gè)具體的例子來說明册踩。假設(shè)Python解釋器執(zhí)行到標(biāo)記為3的地方。解釋器正在foo函數(shù)的調(diào)用中效拭,它接著調(diào)用bar暂吉。下面是frame調(diào)用棧,塊棧和數(shù)據(jù)棧的示意圖缎患。我們感興趣的是解釋器先從最底下的foo()開始慕的,接著執(zhí)行foo的函數(shù)體,然后到達(dá)bar挤渔。

現(xiàn)在肮街,解釋器在bar函數(shù)的調(diào)用中。調(diào)用棧中有3個(gè)fram:一個(gè)對(duì)應(yīng)于module層判导,一個(gè)對(duì)應(yīng)函數(shù)foo,別一個(gè)對(duì)應(yīng)函數(shù)bar嫉父。(Figure 1.2)一旦bar返回沛硅,與它對(duì)應(yīng)的frame就會(huì)從調(diào)用棧中彈出并丟棄。

字節(jié)碼指令RETURN_VALUE告訴解釋器在frame間傳遞一個(gè)值熔号。首先稽鞭,它把位于調(diào)用棧棧頂?shù)膄rame中的數(shù)據(jù)棧的棧頂值彈出。然后把整個(gè)frame彈出丟棄引镊。最后把這個(gè)值壓到下一個(gè)frame的數(shù)據(jù)棧中。

當(dāng)Ned Batchelder和我在寫B(tài)yterun時(shí)篮条,很長(zhǎng)一段時(shí)間我們的實(shí)現(xiàn)中一直有個(gè)重大的錯(cuò)誤弟头。我們整個(gè)虛擬機(jī)中只有一個(gè)數(shù)據(jù)棧,而不是每個(gè)frame都有個(gè)一個(gè)涉茧。我們做了很多測(cè)試赴恨,同時(shí)在Byterun和真正的Python上,為了保證結(jié)果一致伴栓。我們幾乎通過了所有測(cè)試伦连,只有一樣?xùn)|西不能通過,那就是生成器钳垮。最后惑淳,通過仔細(xì)的閱讀Cpython的源碼,我們發(fā)現(xiàn)了錯(cuò)誤所在饺窿。把數(shù)據(jù)棧移到每個(gè)frame就解決了這個(gè)問題歧焦。

回頭在看看這個(gè)bug,我驚訝的發(fā)現(xiàn)Python真的很少依賴于每個(gè)frame有一個(gè)數(shù)據(jù)棧這個(gè)特性肚医。在Python中幾乎所有的操作都會(huì)清空數(shù)據(jù)棧绢馍,所以所有的frame公用一個(gè)數(shù)據(jù)棧是沒問題的。在上面的例子中肠套,當(dāng)bar執(zhí)行完后舰涌,它的數(shù)據(jù)棧為空。即使foo公用這一個(gè)棧你稚,它的值也不會(huì)受影響瓷耙。然而,對(duì)應(yīng)生成器入宦,一個(gè)關(guān)鍵的特點(diǎn)是它能暫停一個(gè)frame的執(zhí)行哺徊,返回到其他的frame,一段時(shí)間后它能返回到原來的frame乾闰,并以它離開時(shí)的同樣的狀態(tài)繼續(xù)執(zhí)行落追。

Byterun

現(xiàn)在我們有足夠的Python解釋器的知識(shí)背景去考察Byterun。

Byterun中有四種對(duì)象涯肩。

  • VirtualMachine類轿钠,它管理高層結(jié)構(gòu)巢钓,frame調(diào)用棧,指令到操作的映射疗垛。這是一個(gè)比前面Inteprter對(duì)象更復(fù)雜的版本症汹。

  • Frame類,每個(gè)Frame類都有一個(gè)code object贷腕,并且管理者其他一些必要的狀態(tài)信息背镇,全局和局部命名空間,指向調(diào)用它的frame的指針和最后執(zhí)行的字節(jié)碼指令泽裳。

  • Function類瞒斩,它被用來代替真正的Python函數(shù)′套埽回想一下胸囱,調(diào)用函數(shù)時(shí)會(huì)創(chuàng)建一個(gè)新的frame。我們自己實(shí)現(xiàn)Function瀑梗,所以我們控制新frame的創(chuàng)建烹笔。

  • Block類,它只是包裝了代碼塊的3個(gè)屬性抛丽。(代碼塊的細(xì)節(jié)不是解釋器的核心谤职,我們不會(huì)花時(shí)間在它身上,把它列在這里铺纽,是因?yàn)锽yterun需要它柬帕。)

The VirtualMachine Class

程序運(yùn)行時(shí)只有一個(gè)VirtualMachine被創(chuàng)建,因?yàn)槲覀冎挥幸粋€(gè)解釋器狡门。VirtualMachine保存調(diào)用棧陷寝,異常狀態(tài),在frame中傳遞的返回值其馏。它的入口點(diǎn)是run_code方法凤跑,它以編譯后的code
object為參數(shù),以創(chuàng)建一個(gè)frame為開始叛复,然后運(yùn)行這個(gè)frame仔引。這個(gè)frame可能再創(chuàng)建出新的frame;調(diào)用棧隨著程序的運(yùn)行增長(zhǎng)縮短褐奥。當(dāng)?shù)谝粋€(gè)frame返回時(shí)咖耘,執(zhí)行結(jié)束。

The Frame Class

接下來撬码,我們來寫Frame對(duì)象儿倒。frame是一個(gè)屬性的集合,它沒有任何方法。前面提到過夫否,這些屬性包括由編譯器生成的code object彻犁;局部,全局和內(nèi)置命名空間凰慈;前一個(gè)frame的引用汞幢;一個(gè)數(shù)據(jù)棧;一個(gè)塊棧微谓;最后執(zhí)行的指令森篷。(對(duì)于內(nèi)置命名空間我們需要多做一點(diǎn)工作,Python對(duì)待這個(gè)命名空間不同豺型;但這個(gè)細(xì)節(jié)對(duì)我們的虛擬機(jī)不重要疾宏。)

接著,我們?cè)谔摂M機(jī)中增加對(duì)frame的操作触创。這有3個(gè)幫助函數(shù):一個(gè)創(chuàng)建新的frame的方法,和壓棧和出棧的方法为牍。第四個(gè)函數(shù)哼绑,run_frame,完成執(zhí)行frame的主要工作,待會(huì)我們?cè)儆懻撨@個(gè)方法碉咆。

The Function Class

Function的實(shí)現(xiàn)有點(diǎn)扭曲抖韩,但是大部分的細(xì)節(jié)對(duì)理解解釋器不重要。重要的是當(dāng)調(diào)用函數(shù)時(shí) -— __call__方法被調(diào)用 -— 它創(chuàng)建一個(gè)新的Frame并運(yùn)行它疫铜。

接著茂浮,回到VirtualMachine對(duì)象,我們對(duì)數(shù)據(jù)棧的操作也增加一些幫助方法壳咕。字節(jié)碼操作的椣浚總是在當(dāng)前frame的數(shù)據(jù)棧。這讓我們能完成POP_TOP,LOAD_FAST,并且讓其他操作棧的指令可讀性更高谓厘。

在我們運(yùn)行frame之前幌羞,我們還需兩個(gè)方法。

第一個(gè)方法竟稳,parse_byte_and_args,以一個(gè)字節(jié)碼為輸入属桦,先檢查它是否有參數(shù),如果有他爸,就解析它的參數(shù)聂宾。這個(gè)方法同時(shí)也更新frame的last_instruction屬性,它指向最后執(zhí)行的指令诊笤。一條沒有參數(shù)的指令只有一個(gè)字節(jié)長(zhǎng)度系谐,而有參數(shù)的字節(jié)有3個(gè)字節(jié)長(zhǎng)。參數(shù)的意義依賴于指令是什么盏混。比如蔚鸥,前面說過惜论,指令POP_JUMP_IF_FALSE,它的參數(shù)指的是跳轉(zhuǎn)目標(biāo)。BUILD_LIST, 它的參數(shù)是列表的個(gè)數(shù)止喷。LOAD_CONST,它的參數(shù)是常量的索引馆类。

一些指令用簡(jiǎn)單的數(shù)字作為參數(shù)。對(duì)于另一些弹谁,虛擬機(jī)需要一點(diǎn)努力去發(fā)現(xiàn)它意義乾巧。標(biāo)準(zhǔn)庫中的dismodule中有一個(gè)備忘單解釋什么參數(shù)有什么意思,這讓我們的代碼更加簡(jiǎn)潔预愤。比如沟于,列表dis.hasname告訴我們LOAD_NAME, IMPORT_NAME,LOAD_GLOBAL,和另外的9個(gè)指令都有同樣的意思:名字列表的索引。

下一個(gè)方法是dispatch,它查看給定的指令并執(zhí)行相應(yīng)的操作植康。在CPython中旷太,這個(gè)分派函數(shù)用一個(gè)巨大的switch語句完成,有超過1500行的代碼销睁。幸運(yùn)的是供璧,我們用的是Python,我們的代碼會(huì)簡(jiǎn)潔的多冻记。我們會(huì)為每一個(gè)字節(jié)碼名字定義一個(gè)方法睡毒,然后用getattr來查找。就像我們前面的小解釋器一樣冗栗,如果一條指令叫做FOO_BAR演顾,那么它對(duì)應(yīng)的方法就是byte_FOO_BAR。現(xiàn)在隅居,我們先把這些方法當(dāng)做一個(gè)黑盒子钠至。每個(gè)指令方法都會(huì)返回None或者一個(gè)字符串why,有些情況下虛擬機(jī)需要這個(gè)額外why信息。這些指令方法的返回值军浆,僅作為解釋器狀態(tài)的內(nèi)部指示棕洋,千萬不要和執(zhí)行frame的返回值相混淆。

The Block Class

在我們完成每個(gè)字節(jié)碼方法前乒融,我們簡(jiǎn)單的討論一下塊掰盘。一個(gè)塊被用于某種控制流,特別是異常處理和循環(huán)赞季。它負(fù)責(zé)保證當(dāng)操作完成后數(shù)據(jù)棧處于正確的狀態(tài)愧捕。比如,在一個(gè)循環(huán)中申钩,一個(gè)特殊的迭代器會(huì)存在棧中次绘,當(dāng)循環(huán)完成時(shí)它從棧中彈出。解釋器需要檢查循環(huán)仍在繼續(xù)還是已經(jīng)停止。

為了跟蹤這些額外的信息邮偎,解釋器設(shè)置了一個(gè)標(biāo)志來指示它的狀態(tài)管跺。我們用一個(gè)變量why實(shí)現(xiàn)這個(gè)標(biāo)志,它可以None或者是下面幾個(gè)字符串這一禾进,"continue",
"break","excption",return豁跑。他們指示對(duì)塊棧和數(shù)據(jù)棧進(jìn)行什么操作⌒涸疲回到我們迭代器的例子艇拍,如果塊棧的棧頂是一個(gè)loop塊,whycontinue,迭代器就因該保存在數(shù)據(jù)棧上宠纯,不是如果whybreak,迭代器就會(huì)被彈出卸夕。

塊操作的細(xì)節(jié)比較精密,我們不會(huì)花時(shí)間在這上面婆瓜,但是有興趣的讀者值得仔細(xì)的看看快集。

The Instructions

剩下了的就是完成那些指令方法了:byte_LOAD_FAST,byte_BINARY_MODULO等等。而這些指令的實(shí)現(xiàn)并不是很有趣廉白,這里我們只展示了一小部分碍讨,完整的實(shí)現(xiàn)在這兒(足夠執(zhí)行我們前面所述的所有代碼了。)

Dynamic Typing: What the Compiler Doesn’t Know

你可能聽過Python是一種動(dòng)態(tài)語言 -— 是它是動(dòng)態(tài)類型的蒙秒。在我們建造解釋器的過程中,已經(jīng)流露出這個(gè)描述宵统。

動(dòng)態(tài)的一個(gè)意思是很多工作在運(yùn)行時(shí)完成晕讲。前面我們看到Python的編譯器沒有很多關(guān)于代碼真正做什么的信息。舉個(gè)例子马澈,考慮下面這個(gè)簡(jiǎn)單的函數(shù)mod瓢省。它區(qū)兩個(gè)參數(shù),返回它們的磨運(yùn)算值痊班。從它的字節(jié)碼中勤婚,我們看到變量ab首先被加載,然后字節(jié)碼BINAY_MODULO完成這個(gè)模運(yùn)算涤伐。

計(jì)算19 % 5得4馒胆,-— 一點(diǎn)也不奇怪。如果我們用不同類的參數(shù)呢凝果?

剛才發(fā)生了什么祝迂?你可能見過這樣的語法,格式化字符串器净。

用符號(hào)%去格式化字符串會(huì)調(diào)用字節(jié)碼BUNARY_MODULO.它取棧頂?shù)膬蓚€(gè)值求模型雳,不管這兩個(gè)值是字符串,數(shù)字或是你自己定義的類的實(shí)例。字節(jié)碼在函數(shù)編譯時(shí)生成(或者說纠俭,函數(shù)定義時(shí))相同的字節(jié)碼會(huì)用于不同類的參數(shù)沿量。

Python的編譯器關(guān)于字節(jié)碼的功能知道的很少。而取決于解釋器來決定BINAYR_MODULO應(yīng)用于什么類型的對(duì)象并完成真確的操作冤荆。這就是為什么Python被描述為動(dòng)態(tài)類型:直到你運(yùn)行前你不必知道這個(gè)函數(shù)參數(shù)的類型朴则。相反,在一個(gè)靜態(tài)類型語言中匙赞,程序員需要告訴編譯器參數(shù)的類型是什么(或者編譯器自己推斷出參數(shù)的類型佛掖。)

編譯器的無知是優(yōu)化Python的一個(gè)挑戰(zhàn) -—
只看字節(jié)碼,而不真正運(yùn)行它涌庭,你就不知道每條字節(jié)碼在干什么芥被!你可以定義一個(gè)類,實(shí)現(xiàn)__mod__方法坐榆,當(dāng)你對(duì)這個(gè)類的實(shí)例使用%時(shí)拴魄,Python就會(huì)自動(dòng)調(diào)用這個(gè)方法。所以席镀,BINARY_MODULO其實(shí)可以運(yùn)行任何代碼匹中。

看看下面的代碼,第一個(gè)a % b看起來沒有用豪诲。

不幸的是顶捷,對(duì)這段代碼進(jìn)行靜態(tài)分析 -— 不運(yùn)行它 -— 不能確定第一個(gè)a % b沒有做任何事。用
%調(diào)用__mod__可能會(huì)寫一個(gè)文件屎篱,或是和程序的其他部分交互服赎,或者其他任何可以在Python中完成的事。很難優(yōu)化一個(gè)你不知道它會(huì)做什么的函數(shù)交播。在Russell
Power和Alex
Rubinsteyn的優(yōu)秀論文中寫道重虑,”我們可用多快的速度解釋Python?”秦士,他們說缺厉,”在普遍缺乏類型信息下,每條指令必須被看作一個(gè)INVOKE_ARBITRARY_METHOD隧土√嵴耄”

Conclusion

Byterun是一個(gè)比CPython容易理解的簡(jiǎn)潔的Python解釋器。Byterun復(fù)制了CPython的主要結(jié)構(gòu):一個(gè)基于棧的指令集稱為字節(jié)碼曹傀,它們順序執(zhí)行或在指令間跳轉(zhuǎn)关贵,向棧中壓入和從中彈出數(shù)據(jù)。解釋器隨著函數(shù)和生成器的調(diào)用和返回卖毁,動(dòng)態(tài)的創(chuàng)建揖曾,銷毀frame落萎,并在frame間跳轉(zhuǎn)。Byterun也有著和真正解釋器一樣的限制:因?yàn)镻ython使用動(dòng)態(tài)類型炭剪,解釋器必須在運(yùn)行時(shí)決定指令的正確行為练链。

我鼓勵(lì)你去反匯編你的程序,然后用Byterun來運(yùn)行奴拦。你很快會(huì)發(fā)現(xiàn)這個(gè)縮短版的Byterun所沒有實(shí)現(xiàn)的指令媒鼓。完整的實(shí)現(xiàn)在這里或者仔細(xì)閱讀真正的CPython解釋器ceval.c,你也可以實(shí)現(xiàn)自己的解釋器!

Acknowledgements

Thanks to Ned Batchelder for originating this project and guiding my?contributions, Michael Arntzenius for his help debugging the code and editing?the prose, Leta Montopoli for her edits, and the entire Recurse Center?community for their support and interest. Any errors are my own.

本文原文鏈接為:http://qingyunha.github.io/taotao/

歡迎轉(zhuǎn)發(fā)至朋友圈错妖。如無特殊注明绿鸣,本公號(hào)所發(fā)文章均為原創(chuàng)或編譯,如需轉(zhuǎn)載暂氯,請(qǐng)聯(lián)系「編程派」獲得授權(quán)潮模。

【近期優(yōu)秀教程推薦】

使用好鏡像源,把等待的時(shí)間轉(zhuǎn)為生產(chǎn)力

用Python從頭開發(fā)一個(gè)自己的Shell(上)

不懂排序算法痴施?看這些舞者給你做的演示(下)

一文學(xué)會(huì)Python多進(jìn)程編程

一文學(xué)會(huì)Python多線程編程

掃碼關(guān)注編程派

獲取最新教程及資源推送


↓↓↓ 點(diǎn)擊閱讀原文擎厢,查看更多Python教程


閱讀原文:http://mp.weixin.qq.com/s?timestamp=1477798584&src=3&ver=1&signature=6dj9T6jz1W8f0nSLZlvTSgYlX*SDzt4ceMVqdegFL0SShbzXYdj7QSzwBPDo1AMBDNWLxXv6qHmNoF7RKWWPPNIeMG9h1DgHvuBJ4mAuBbMzaIgGLgIM8Cjrh6Mb2vQYe-eQO29e8X80B-KtvQciXpkSZ6CLM7p8Y8OXZ*zeIgc=
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辣吃,隨后出現(xiàn)的幾起案子动遭,更是在濱河造成了極大的恐慌,老刑警劉巖神得,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厘惦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡哩簿,警方通過查閱死者的電腦和手機(jī)绵估,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卡骂,“玉大人,你說我怎么就攤上這事形入∪纾” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵亿遂,是天一觀的道長(zhǎng)浓若。 經(jīng)常有香客問我,道長(zhǎng)蛇数,這世上最難降的妖魔是什么挪钓? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮耳舅,結(jié)果婚禮上碌上,老公的妹妹穿的比我還像新娘倚评。我一直安慰自己,他們只是感情好馏予,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布天梧。 她就那樣靜靜地躺著,像睡著了一般霞丧。 火紅的嫁衣襯著肌膚如雪呢岗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天蛹尝,我揣著相機(jī)與錄音后豫,去河邊找鬼。 笑死突那,一個(gè)胖子當(dāng)著我的面吹牛挫酿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陨收,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼饭豹,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了务漩?” 一聲冷哼從身側(cè)響起拄衰,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饵骨,沒想到半個(gè)月后翘悉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡居触,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年妖混,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片轮洋。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡制市,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弊予,到底是詐尸還是另有隱情祥楣,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布汉柒,位于F島的核電站误褪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏碾褂。R本人自食惡果不足惜兽间,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望正塌。 院中可真熱鬧嘀略,春花似錦恤溶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逮壁,卻和暖如春孵坚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窥淆。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工卖宠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忧饭。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓扛伍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親词裤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刺洒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容