您可以在Python语法中添加新语句吗?

您可以在Python语法中添加新语句吗?,第1张

您可以在Python语法中添加新语句吗?

您可能会发现这很有用-Python内部:在Python上添加新语句,引用如下:


本文旨在更好地了解Python前端的工作方式。仅仅阅读文档和源代码可能会有点无聊,因此我在这里采用动手实践的方法:我将向

until
Python添加一条语句。

本文的所有编码都是针对Python
Mercurial存储库镜像中
最前沿的Py3k分支完成的。

until
声明

有些语言,如红宝石,有一个

until
说法,这是补充
while
until num == 0
相当于
while num !=0
)。在Ruby中,我可以这样写:

num = 3until num == 0 do  puts num  num -= 1end

它将打印:

321

因此,我想为Python添加类似的功能。也就是说,能够写:

num = 3until num == 0:  print(num)  num -= 1
语言倡导题外话

本文并不试图建议在

until
Python中添加一条语句。尽管我认为这样的声明可以使一些代码更清晰,并且本文显示了添加的难易程度,但我完全尊重Python的极简主义哲学。实际上,我在这里要做的只是深入了解Python的内部工作原理。

修改语法

Python使用名为的自定义解析器生成器

pgen
。这是一个LL(1)解析器,它将Python源代码转换为解析树。解析器生成器的输入是文件
Grammar/Grammar

[1] 。这是一个简单的文本文件,用于指定Python的语法。

[1] :从此处开始,相对于源代码树的根目录(在运行configure和make生成Python的目录)中将对Python源文件的引用赋予相对性。

必须对语法文件进行两次修改。首先是为

until
语句添加定义。我找到了该
while
语句的定义位置(
while_stmt
),并添加
until_stmt
到了
[2] 下面:

compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decoratedif_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]while_stmt: 'while' test ':' suite ['else' ':' suite]until_stmt: 'until' test ':' suite

[2] :这演示了在修改我不熟悉的源代码时使用的一种通用技术: 按相似性工作
。这个原则并不能解决您的所有问题,但绝对可以简化流程。由于必须完成的所有工作

while
都必须完成
until
,因此它可以作为很好的指导。

请注意,我已经决定

else
从我的定义中排除该子句
until
,只是为了使它有所不同(并且因为坦率地说,我不喜欢
else
循环的子句,并且认为它与Python的Zen不太匹配)。

第二个更改是将规则修改为

compound_stmt
include
until_stmt
,如您在上面的代码片段中所见。紧接着
while_stmt
又是。

当你运行

make
修改后
Grammar/Grammar
,通知该
pgen
程序运行重新生成
Include/graminit.h
Python/graminit.c
,然后几个文件得到重新编译。

修改AST生成代码

在Python解析器创建了一个解析树之后,该树将转换为AST,因为在编译过程的后续阶段中使用AST更加容易。

因此,我们将访问

Parser/Python.asdl
,它定义了Python
AST的结构,并为我们的新
until
语句添加了一个AST节点,该语句又位于以下位置
while

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

如果您现在运行

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.h
和中
Python/Python-ast.c
)时,我们仍然必须编写代码,以手动将相关的解析树节点转换为它。这是在文件中完成的
Python/ast.c
。在那里,一个名为的函数
ast_for_stmt
将语句的解析树节点转换为AST节点。同样,在我们的老朋友的指导下
while
,我们跳入
switch
了处理复合语句的大幕,并为
until_stmt
以下项添加了一个子句:

case while_stmt:    return ast_for_while_stmt(c, ch);case until_stmt:    return ast_for_until_stmt(c, ch);

现在我们应该执行

ast_for_until_stmt
。这里是:

static stmt_tyast_for_until_stmt(struct compiling *c, const node *n){        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
,所不同的是,
until
我决定不支持该
else
子句。如预期的那样,使用其他AST创建函数(如
ast_for_expr
条件表达式和语句
ast_for_suite
主体)以递归方式创建AST
until
。最后,
Until
返回一个名为的新节点。

请注意,我们

n
使用诸如
NCH
和的宏来访问解析树节点
CHILD
。这些值得理解-它们的代码在
Include/node.h

题外话:AST组成

我选择为该

until
语句创建一种新型的AST ,但实际上这不是必需的。我可以使用现有AST节点的组成来节省一些工作并实现新功能,因为:

until condition:   # do stuff

在功能上等同于:

while not condition:  # do stuff

与其在中创建

Until
节点
ast_for_until_stmt
,不如创建一个节点作为子
Not
节点的
While
节点。由于AST编译器已经知道如何处理这些节点,因此可以跳过该过程的后续步骤。

将AST编译成字节码

下一步是将AST编译为Python字节码。编译产生的中间结果是CFG(控制流图),但是由于使用相同的代码进行处理,因此我暂时将忽略此细节,并留给另一篇文章。

我们接下来要看的代码是

Python/compile.c
。按照的开头
while
,我们找到函数
compiler_visit_stmt
,该函数负责将语句编译为字节码。我们为添加一个子句
Until

case While_kind:    return compiler_while(c, s);case Until_kind:    return compiler_until(c, s);

如果您想知道

Until_kind
是什么,它是一个
_stmt_kind
从AST定义文件自动生成为的常数(实际上是枚举的值)
Include/Python-ast.h
。无论如何,我们称
compiler_until
它当然仍然不存在。我待会儿。

如果您像我一样好奇,您会发现这

compiler_visit_stmt
很奇怪。
grep
-ping源树的数量并没有揭示调用它的地方。在这种情况下,仅保留一个选项-C
macro-fu。确实,经过简短的调查,我们找到了以下
VISIT
宏中定义的宏
Python/compile.c

#define VISIT(C, TYPE, V) {    if (!compiler_visit_ ## TYPE((C), (V)))         return 0; 

它用来调用

compiler_visit_stmt
compiler_body
。回到我们的业务,但是…

如所承诺的,这是

compiler_until

static intcompiler_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字节码的深刻理解而编写的。像本文的其余部分一样,它是模仿亲属

compiler_while
功能来完成的。但是,通过仔细阅读它,牢记Python
VM是基于堆栈的,并浏览该
dis
模块的文档(该模块的文档提供了带说明的Python字节码列表),可以了解正在发生的事情。

就是这样,我们完成了……不是吗?

进行所有更改并运行之后

make
,我们可以运行新编译的Python并尝试新的
until
语句:

>>> until num == 0:...   print(num)...   num -= 1...321

瞧,行得通!让我们看看使用

dis
模块为新语句创建的字节码,如下所示:

import disdef myfoo(num):    until num == 0:        print(num)        num -= 1dis.dis(myfoo)

结果如下:

40 SETUP_LOOP   36 (to 39)      >>    3 LOAD_FAST     0 (num) 6 LOAD_ConST    1 (0) 9 COMPARE_OP    2 (==)12 POP_JUMP_IF_TRUE        385          15 LOAD_NAME     0 (print)18 LOAD_FAST     0 (num)21 CALL_FUNCTION 124 POP_TOP6          25 LOAD_FAST     0 (num)28 LOAD_ConST    2 (1)31 INPLACE_SUBTRACT32 STORE_FAST    0 (num)35 JUMP_ABSOLUTE 3      >>   38 POP_BLOCK      >>   39 LOAD_ConST    0 (None)42 RETURN_VALUE

最有趣的 *** 作是数字12:如果条件为真,我们跳到循环之后。这是的正确语义

until
。如果未执行该跳转,则循环主体将继续运行,直到其跳回到 *** 作35中的状态为止。

我对更改感到满意,然后尝试运行该函数(执行

myfoo(3)
),而不显示其字节码。结果令人鼓舞:

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'

哇…这不好。那么出了什么问题?

缺少符号表的情况

Python编译器在编译AST时执行的步骤之一是为其编译的代码创建符号表。对

PySymtable_Build
in的调用将
PyAST_Compile
调用符号表模块(
Python/symtable.c
),该模块以类似于代码生成功能的方式遍历AST。每个作用域都有一个符号表,有助于编译器找出一些关键信息,例如哪些变量是全局变量,哪些是局部变量。

为了解决这个问题,我们必须修改的

symtable_visit_stmt
函数,在类似语句 [3]的*
代码之后
Python/symtable.c
添加用于处理
until
语句的代码:
while
*

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;

[3]
:顺便说一句,没有此代码,将会有的编译器警告

Python/symtable.c
。编译器注意到,
Until_kind
枚举值未在和的switch语句中处理
symtable_visit_stmt
。检查编译器警告始终很重要!

现在我们真的完成了。进行此更改后,编译源

myfoo(3)
将按预期执行工作。

结论

在本文中,我演示了如何向Python添加新语句。尽管需要对Python编译器的代码进行大量修改,但更改并不难实现,因为我使用了类似的现有语句作为准则。

Python编译器是一种复杂的软件,我并不声称自己是该领域的专家。但是,我对Python的内部结构特别是前端非常感兴趣。因此,我发现此练习对于编译器原理和源代码的理论研究非常有用。它将作为以后将深入编译器的文章的基础。

参考文献

我使用了一些出色的参考来构建本文。在这里,它们没有特定的顺序:

  • PEP 339:CPython编译器的设计-可能是Python编译器最重要,最全面的 官方 文档。太短了,它痛苦地显示出缺少Python内部结构的良好文档的匮乏。
  • “ Python编译器内部知识”-Thomas Lee的文章
  • “ Python:设计与实现”-Guido van Rossum的演示
  • Python(2.5)虚拟机,导览-PeterTröger的演示

原始资料



欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5616608.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存