现在的位置: 首页 > 综合 > 正文

走进Python: 为Python增加新语法

2013年03月29日 ⁄ 综合 ⁄ 共 4255字 ⁄ 字号 评论关闭

原文地址:http://eli.thegreenplace.net/2010/06/30/python-internals-adding-a-new-statement-to-python/

译文地址:http://everet.org/2012/07/add-new-grammer-to-python.html

译者:Stupid
ET

翻译得比较仓储,里面会有些语句不通顺,请见谅,日后会慢慢重构。

修改后的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总,我可以这样写:

1
2
3
4
5
num = 3
until num == 0 do
  puts num
  num -= 1
end

它会输出

1
2
3
3
2
1

所以,我想要添加一个类似的功能到Python。也就是说,能够写成这样:

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

 

A language-advocacy digression(不知如何翻译)

本文并没有企图建议添加一个Until语句到Python。虽然我认为这样的语句会让一些代码清晰,而且这篇文章也展示了这是多么容易为Python添加这样的语句,但我非常尊重Python的简约主义的哲学。所以我在这里做的一切,仅仅是为了更能了解Python的内部工作原理。

修改语法

Python使用一个自定义解析器生成器pgen。这是一个LL(1)的解析器,用于将Python源代码转换成一个解析树。解析器生成器的输入文件  Grammar/Grammar [1]。这是一个简单的文本文件,用于定义Python的语法。
我们对这个语法文件进行了两处修改。第一个是添加until语句的定义。我发现那里的while语句定义为(while_stmt),于是我们在下面补充until_stmt[2]:

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子句,只是为了让他们有点不同(因为,坦率地说,我不喜欢循环的else子句,认为它有悖于the Zen of Python)。

第二个变化是修改规则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的解析器创建了一个解析树后,这棵树被转换成一个AST(译注:抽象语法树),因为AST让后续的编译流程更简单

所以,我们打开Parser/Python.asdl,它定义了结构的Python的抽象语法树,我们在那里为我们新增的until语句添加一个AST节点,又放在while的右后方:

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.

如果你现在运行make,请注意在编译一堆文件之前, 运行Parser/asdl_c.py根据AST定义文件生成的C代码。这(如Grammar/Grammar)是另一个Python源代码使用迷你语言(换句话说,一个DSL)来简化编程的例子。还请注意,由于Parser/asdl_c.py是一个Python脚本,这是一种自举——从原型中构建Python。Python已经拥有自举的能力了。

虽然Parser/asdl_c.py生成的代码管理着我们的新定义的AST节点(生成到文件Include/Python-ast.hPython/Python-ast.c中),我们仍然需要编写的代码,将一个相关的解析树节点转换成我们新定义的AST节点。

(译注:cedricporter@Stupid-ET:~/projects/python2.7-2.7.2/Parser$ ./asdl_c.py -h ../Include/ Python.asdl )

这些工作在 Python/ast.c中完成。在那里,一个叫做 ast_for_stmt的函数将解析树节点转换为AST节点。我们再次在我们的老朋友while的引导下,进入处理compound_stmt的庞大的switch中,为until增加一个子块:

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

现在我们要实现ast_for_until_stmt:

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创建函数像ast_for_expr对于条件表达式和 ast_for_suite来递归地创建AST。最后,一个until新节点被创建返回。

注意,我们通过一些宏,像NCHCHILD来访问解析树节点。这些都是值得我们去理解——他们的代码在Include/node.h.

题外话:AST组合

我选择创建一个新until类型的AST,但实际上这是没有必要的。虽然我能通过实现组合现有的AST节点来节省一些工作:

1
2
until condition:
   # do stuff

功能上等价于:

1
2
while not condition:
  # do stuff

与其在ast_until_stmt里面创建一个新的Until节点,我可以创建一个Not节点下面挂上While节点。因为AST解释器已经知道如何处理这些节点,所以下一步可以跳过了。

将AST变成字节码

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解析成字节码。编译过程中有一个中间结果CFG(控制流图),但由于有相同的代码处理它,所以我暂时先忽略这一细节,留到另一篇文章再讲解。

下一步,们将看看Python/compile.c。在while的带领下,我们找到负责将语句编译成字节码的函数compiler_visit_stmt。在这里,我们为Until添加一个子句:

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

【上篇】
【下篇】

抱歉!评论已关闭.