小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

走進Python: 為Python增加新語法

 天才白癡書館 2015-05-02

走進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
1
2
3
4
5
num = 3
until num == 0 do
  puts num
  num -= 1
end

它會輸出

python
1
2
3
3
2
1

所以,我想要添加一個類似的功能到Python。也就是說,能夠?qū)懗蛇@樣:

python
1
2
3
4
num = 3
until num == 0:
  print(num)
  num -= 1

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]:

python
1
2
3
4
compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite

注意,我決定了從我定義的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的右后方:

python
1
2
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)

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.hPython/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增加一個子塊:

c
1
2
3
4
case while_stmt:
    return ast_for_while_stmt(c, ch);
case until_stmt:
    return ast_for_until_stmt(c, ch);

現(xiàn)在我們要實現(xiàn)ast_for_until_stmt:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
    /* until_stmt: 'until' test ':' suite */
    REQ(n, until_stmt);

    if (NCH(n) == 4) {
        expr_ty expression;
        asdl_seq *suite_seq;

        expression = ast_for_expr(c, CHILD(n, 1));
        if (!expression)
            return NULL;
        suite_seq = ast_for_suite(c, CHILD(n, 3));
        if (!suite_seq)
            return NULL;
        return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
    }

    PyErr_Format(PyExc_SystemError,
                 "wrong number of tokens for 'until' statement: %d",
                 NCH(n));
    return NULL;
}

再一次,這是看起來像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é)省一些工作:

python
1
2
until condition:
   # do stuff

功能上等價于:

python
1
2
while not condition:
  # do stuff

與其在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添加一個子句:

c
1
2
3
4
case While_kind:
    return compiler_while(c, s);
case Until_kind:
    return compiler_until(c, s);

想必你也想知道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:

c
1
2
3
#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
        return 0; \

在compiler_body中,它是用來調(diào)用compiler_visit_stmt的。

正如之前說的那樣,我們在這里給出compiler_until:

c
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
static int
compiler_until(struct compiler *c, stmt_ty s)
{
    basicblock *loop, *end, *anchor = NULL;
    int constant = expr_constant(s->v.Until.test);

    if (constant == 1) {
        return 1;
    }
    loop = compiler_new_block(c);
    end = compiler_new_block(c);
    if (constant == -1) {
        anchor = compiler_new_block(c);
        if (anchor == NULL)
            return 0;
    }
    if (loop == NULL || end == NULL)
        return 0;

    ADDOP_JREL(c, SETUP_LOOP, end);
    compiler_use_next_block(c, loop);
    if (!compiler_push_fblock(c, LOOP, loop))
        return 0;
    if (constant == -1) {
        VISIT(c, expr, s->v.Until.test);
        ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
    }
    VISIT_SEQ(c, stmt, s->v.Until.body);
    ADDOP_JABS(c, JUMP_ABSOLUTE, loop);

    if (constant == -1) {
        compiler_use_next_block(c, anchor);
        ADDOP(c, POP_BLOCK);
    }
    compiler_pop_fblock(c, LOOP, loop);
    compiler_use_next_block(c, end);

    return 1;
}

我必須得承認,這些代碼是在我沒有深刻理解Python字節(jié)碼的前提下編寫的。就像接下來的文章那樣,它僅僅是模仿它的親戚函數(shù)compiler_while。我們通過仔細閱讀,知道Python虛擬機是基于棧的,大致看了一下dis模塊的文檔,發(fā)現(xiàn)那里有一系列Python字節(jié)碼的描述.

嗯!我們完成了,不是嗎?

在修改完后,我們運行make,然后我們運行我們新編譯出來的Python來測試我們新增的until語句:

python
1
2
3
4
5
6
7
>>> until num == 0:
...   print(num)
...   num -= 1
...
3
2
1

瞧!它能夠工作!我們通過dis模塊來看看新語句的字節(jié)碼:

python
1
2
3
4
5
6
7
8
import dis

def myfoo(num):
    until num == 0:
        print(num)
        num -= 1

dis.dis(myfoo)

Here’s the result:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
4           0 SETUP_LOOP              36 (to 39)
      >>    3 LOAD_FAST                0 (num)
            6 LOAD_CONST               1 (0)
            9 COMPARE_OP               2 (==)
           12 POP_JUMP_IF_TRUE        38

5          15 LOAD_NAME                0 (print)
           18 LOAD_FAST                0 (num)
           21 CALL_FUNCTION            1
           24 POP_TOP

6          25 LOAD_FAST                0 (num)
           28 LOAD_CONST               2 (1)
           31 INPLACE_SUBTRACT
           32 STORE_FAST               0 (num)
           35 JUMP_ABSOLUTE            3
      >>   38 POP_BLOCK
      >>   39 LOAD_CONST               0 (None)
           42 RETURN_VALUE

最有趣的是編號12的字節(jié)碼:如果條件為真,我們跳轉(zhuǎn)到循環(huán)的后面。這個符合until的語義。如果jump沒有被執(zhí)行,循環(huán)體就繼續(xù)運行,直到它跳轉(zhuǎn)到編號35的字節(jié)碼。

我對我的修改自我感覺良好,于是我繼續(xù)測試這個函數(shù)(執(zhí)行myfoo(3)),結(jié)果并不令人振奮:

python
1
2
3
4
5
6
Traceback (most recent call last):
  File "zy.py", line 9, in
    myfoo(3)
  File "zy.py", line 5, in myfoo
    print(num)
SystemError: no locals when loading 'print'

哇…這個真是悲劇。究竟哪里出錯了?

丟失符號

在解析AST的時候,Python解析器執(zhí)行的步驟之一是構(gòu)建符號表。通過在PyAST_Compile里面調(diào)用PySymtable_Build(Python/symtable.c)來遍歷AST。擁有每一個作用域的符號表有助于編譯器找出一些關鍵的信息,就像哪些變量是全局的,哪些變量是局部的。

我們需要修改Python/symtable.c下的symtable_visit_stmt來解決這個問題,我們添加一些處理until語句的代碼,放在相似的while語句的代碼后面 [3]::

c
1
2
3
4
5
6
7
8
9
10
case While_kind:
    VISIT(st, expr, s->v.While.test);
    VISIT_SEQ(st, stmt, s->v.While.body);
    if (s->v.While.orelse)
        VISIT_SEQ(st, stmt, s->v.While.orelse);
    break;
case Until_kind:
    VISIT(st, expr, s->v.Until.test);
    VISIT_SEQ(st, stmt, s->v.Until.body);
    break;

現(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:

  • PEP 339: Design of the CPython compiler – probably the most important and comprehensive piece of official documentation for the Python compiler. Being very short, it painfully displays the scarcity of good documentation of the internals of Python.
  • “Python Compiler Internals” – an article by Thomas Lee
  • “Python: Design and Implementation” – a presentation by Guido van Rossum
  • Python (2.5) Virtual Machine, A guided tour – a presentation by Peter Tr?ger

[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

您可能也喜歡

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多