走進Python: 為Python增加新語法| Comments 原文地址:http://eli./2010/06/30/python-internals-adding-a-new-statement-to-python/ 譯文地址:http:///2012/07/add-new-grammer-to-python.html 譯者:Stupid ET 翻譯得比較倉儲,里面會有些語句不通順,請見諒,日后會慢慢重構(gòu)。 修改后的Python請見:https://github.com/cedricporter/python2.7-mod/tags ,在Ubuntu下可以正常編譯。 本文的目的是試圖更好地理解Python的前端是如何工作的。如果我們僅僅是閱讀文檔和源代碼,那么可能有點無聊,所以我將親手實踐:為Python添加一個until語句。 這篇文章中的所有的編碼,是針對最新的Py3k分支Python Mercurial repository mirror。 until語句有些語言,像Ruby,擁有until語句,用來補充while語句 (until num == 0 等價與 while num != 0)。在Ruby總,我可以這樣寫:
它會輸出
所以,我想要添加一個類似的功能到Python。也就是說,能夠?qū)懗蛇@樣:
A language-advocacy digression(不知如何翻譯)本文并沒有企圖建議添加一個Until語句到Python。雖然我認為這樣的語句會讓一些代碼清晰,而且這篇文章也展示了這是多么容易為Python添加這樣的語句,但我非常尊重Python的簡約主義的哲學。所以我在這里做的一切,僅僅是為了更能了解Python的內(nèi)部工作原理。 修改語法Python使用一個自定義解析器生成器pgen。這是一個LL(1)的解析器,用于將Python源代碼轉(zhuǎn)換成一個解析樹。解析器生成器的輸入文件 Grammar/Grammar [1]。這是一個簡單的文本文件,用于定義Python的語法。 我們對這個語法文件進行了兩處修改。第一個是添加until語句的定義。我發(fā)現(xiàn)那里的while語句定義為(while_stmt),于是我們在下面補充until_stmt[2]:
注意,我決定了從我定義的until語句中去掉else子句,只是為了讓他們有點不同(因為,坦率地說,我不喜歡循環(huán)的else子句,認為它有悖于the Zen of Python)。 第二個變化是修改規(guī)則compound_stmt,正如上面你所見到的那樣,讓它可以推導成until_stmt。我們把它放在while_stmt的右邊。 當您在修改完Grammar/Grammar后準備運行make時注意運行pgen程序運行時重新生成Include/graminit.h以及Python/graminit.c再重新編譯。 (譯注:cedricporter@Stupid-ET:~/projects/python2.7-2.7.2/Parser$ ./pgen ../Grammar/Grammar graminit.h graminit.c) 修改AST生成代碼在Python的解析器創(chuàng)建了一個解析樹后,這棵樹被轉(zhuǎn)換成一個AST(譯注:抽象語法樹),因為AST讓后續(xù)的編譯流程更簡單。 所以,我們打開Parser/Python.asdl,它定義了結(jié)構(gòu)的Python的抽象語法樹,我們在那里為我們新增的until語句添加一個AST節(jié)點,又放在while的右后方:
If you now run make, notice that before compiling a bunch of files, Parser/asdl_c.py is run to generate C code from the AST definition file. This (like Grammar/Grammar) is another example of the Python source-code using a mini-language (in other words, a DSL) to simplify programming. Also note that since Parser/asdl_c.py is a Python script, this is a kind of bootstrapping – to build Python from scratch, Python already has to be available. 如果你現(xiàn)在運行make,請注意在編譯一堆文件之前, 運行Parser/asdl_c.py根據(jù)AST定義文件生成的C代碼。這(如Grammar/Grammar)是另一個Python源代碼使用迷你語言(換句話說,一個DSL)來簡化編程的例子。還請注意,由于Parser/asdl_c.py是一個Python腳本,這是一種自舉——從原型中構(gòu)建Python。Python已經(jīng)擁有自舉的能力了。 雖然Parser/asdl_c.py生成的代碼管理著我們的新定義的AST節(jié)點(生成到文件Include/Python-ast.h和Python/Python-ast.c中),我們?nèi)匀恍枰帉懙拇a,將一個相關的解析樹節(jié)點轉(zhuǎn)換成我們新定義的AST節(jié)點。 (譯注:cedricporter@Stupid-ET:~/projects/python2.7-2.7.2/Parser$ ./asdl_c.py -h ../Include/ Python.asdl ) 這些工作在 Python/ast.c中完成。在那里,一個叫做 ast_for_stmt的函數(shù)將解析樹節(jié)點轉(zhuǎn)換為AST節(jié)點。我們再次在我們的老朋友while的引導下,進入處理compound_stmt的龐大的switch中,為until增加一個子塊:
現(xiàn)在我們要實現(xiàn)ast_for_until_stmt:
再一次,這是看起來像ast_for_while_stmt,不過不同的是,它不支持else子句。也正如預期的那樣,在until語句的主體中使用其他AST創(chuàng)建函數(shù)像ast_for_expr對于條件表達式和 ast_for_suite來遞歸地創(chuàng)建AST。最后,一個until新節(jié)點被創(chuàng)建返回。 注意,我們通過一些宏,像NCH和CHILD來訪問解析樹節(jié)點。這些都是值得我們?nèi)ダ斫狻麄兊拇a在Include/node.h. 題外話:AST組合我選擇創(chuàng)建一個新until類型的AST,但實際上這是沒有必要的。雖然我能通過實現(xiàn)組合現(xiàn)有的AST節(jié)點來節(jié)省一些工作:
功能上等價于:
與其在ast_until_stmt里面創(chuàng)建一個新的Until節(jié)點,我可以創(chuàng)建一個Not節(jié)點下面掛上While節(jié)點。因為AST解釋器已經(jīng)知道如何處理這些節(jié)點,所以下一步可以跳過了。 將AST變成字節(jié)碼The next step is compiling the AST into Python bytecode. The compilation has an intermediate result which is a CFG (Control Flow Graph), but since the same code handles it I will ignore this detail for now and leave it for another article. 下一步是將AST解析成字節(jié)碼。編譯過程中有一個中間結(jié)果CFG(控制流圖),但由于有相同的代碼處理它,所以我暫時先忽略這一細節(jié),留到另一篇文章再講解。 下一步,們將看看Python/compile.c。在while的帶領下,我們找到負責將語句編譯成字節(jié)碼的函數(shù)compiler_visit_stmt。在這里,我們?yōu)閁ntil添加一個子句:
想必你也想知道Until_kind是什么,它是一個根據(jù)AST定義自動生成到Include/Python-ast.h的常量(實際上是一個_stmt_kind的枚舉)。當然,我們調(diào)用的compiler_until還不存在。我等等就會實現(xiàn)它。 如果你好奇的像我一樣,你會注意到compiler_visit_stmt非常特別。再多的 grep平源樹能揭示它叫。在這種情況下,只有一個選擇仍然macro-fu – C。事實上,一個簡短的調(diào)查使我們進入了 訪問宏定義在 Python / compile.c:
在compiler_body中,它是用來調(diào)用compiler_visit_stmt的。 正如之前說的那樣,我們在這里給出compiler_until:
我必須得承認,這些代碼是在我沒有深刻理解Python字節(jié)碼的前提下編寫的。就像接下來的文章那樣,它僅僅是模仿它的親戚函數(shù)compiler_while。我們通過仔細閱讀,知道Python虛擬機是基于棧的,大致看了一下dis模塊的文檔,發(fā)現(xiàn)那里有一系列Python字節(jié)碼的描述. 嗯!我們完成了,不是嗎?在修改完后,我們運行make,然后我們運行我們新編譯出來的Python來測試我們新增的until語句:
瞧!它能夠工作!我們通過dis模塊來看看新語句的字節(jié)碼:
Here’s the result:
最有趣的是編號12的字節(jié)碼:如果條件為真,我們跳轉(zhuǎn)到循環(huán)的后面。這個符合until的語義。如果jump沒有被執(zhí)行,循環(huán)體就繼續(xù)運行,直到它跳轉(zhuǎn)到編號35的字節(jié)碼。 我對我的修改自我感覺良好,于是我繼續(xù)測試這個函數(shù)(執(zhí)行myfoo(3)),結(jié)果并不令人振奮:
哇…這個真是悲劇。究竟哪里出錯了? 丟失符號在解析AST的時候,Python解析器執(zhí)行的步驟之一是構(gòu)建符號表。通過在PyAST_Compile里面調(diào)用PySymtable_Build(Python/symtable.c)來遍歷AST。擁有每一個作用域的符號表有助于編譯器找出一些關鍵的信息,就像哪些變量是全局的,哪些變量是局部的。 我們需要修改Python/symtable.c下的symtable_visit_stmt來解決這個問題,我們添加一些處理until語句的代碼,放在相似的while語句的代碼后面 [3]::
現(xiàn)在,我們真的完成了。修改后的源碼可以在myfoo(3)運行正常。 結(jié)論在本文中,我展示了如何為Python增加一個新語句。盡管需要比較多處的修改Python編譯器,但是這些修改并不難,因為我跟隨著一個相似的語句來修改。 Python編譯器適宜隔非常復雜的程序,我不想自稱專家。然而,我真的對Python內(nèi)部實現(xiàn)相當感興趣,特別是前端。因此,我發(fā)現(xiàn)這種練習是一個編譯理論與實踐的結(jié)合。它將作為后續(xù)文章的基礎來更深層次地探究編譯器。 參考I used a few excellent references for the construction of this article. Here they are, in no particular order:
[1] From here on, references to files in the Python source are given relatively to the root of the source tree, which is the directory where you run configure and make to build Python. [2] This demonstrates a common technique I use when modifying source code I’m not familiar with: work by similarity. This principle won’t solve all your problems, but it can definitely ease the process. Since everything that has to be done forwhile also has to be done for until, it serves as a pretty good guideline. [3] By the way, without this code there’s a compiler warning for Python/symtable.c. The compiler notices that theUntil_kind enumeration value isn’t handled in the switch statement of symtable_visit_stmt and complains. It’s always important to check for compiler warnings! 本文鏈接: http:///add-new-grammer-to-python.html您可能也喜歡 |
|
|