gpt4 book ai didi

python - 你能在 Python 的语法中添加新的语句吗?

转载 作者:行者123 更新时间:2023-12-01 22:06:25 24 4
gpt4 key购买 nike

你能在 Python 的语法中添加新的语句(比如 printraisewith )吗?

说,让..

mystatement "Something"

或者,
new_if True:
print "example"

如果您应该这样做,则不是那么多,而是如果可能的话(缺少修改 python 解释器代码)

最佳答案

您可能会发现这很有用 - Python internals: adding a new statement to Python ,这里引用:

本文试图更好地理解 Python 的前端是如何工作的。只是阅读文档和源代码可能有点无聊,所以我在这里采取动手的方法:我将添加一个 until对 Python 的声明。

本文的所有编码都是针对 Python Mercurial repository mirror 中的尖端 Py3k 分支完成的。 .
until陈述

一些语言,比如 Ruby,有一个 until声明,这是对 while 的补充( until num == 0 等价于 while num != 0 )。在 Ruby 中,我可以这样写:

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

它会打印:
3
2
1

所以,我想为 Python 添加一个类似的功能。也就是说,能够写:
num = 3
until num == 0:
print(num)
num -= 1

语言倡导离题

本文并不试图建议添加 until对 Python 的声明。虽然我认为这样的声明会使一些代码更清晰,并且本文展示了添加是多么容易,但我完全尊重 Python 的极简主义哲学。我在这里真正想做的就是深入了解 Python 的内部工作原理。

修改语法

Python 使用名为 pgen 的自定义解析器生成器.这是一个 LL(1) 解析器,可将 Python 源代码转换为解析树。解析器生成器的输入是文件 Grammar/Grammar [1] .这是一个简单的文本文件,指定了 Python 的语法。

[1] : 从这里开始,对 Python 源代码中文件的引用相对于源代码树的根目录给出,这是您运行 configure 和 make 以构建 Python 的目录。

必须对语法文件进行两次修改。首先是为 until 添加定义陈述。我找到了 while语句已定义( while_stmt ),并添加 until_stmt下面 [2] :
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

[2] :这演示了我在修改我不熟悉的源代码时使用的常用技术:通过相似性工作。这个原则不能解决你所有的问题,但它绝对可以简化这个过程。由于所有必须为 while 做的事情也必须为 until 做,它是一个很好的指导方针。

请注意,我已决定排除 else条款来自我对 until 的定义,只是为了让它有点不同(坦率地说,我不喜欢循环的 else 子句,并且认为它不太适合 Python 的 Zen)。

第二个变化是修改 compound_stmt的规则包括 until_stmt ,正如您在上面的代码段中看到的那样。就在 while_stmt 之后,再次。

当您运行时 make修改后 Grammar/Grammar ,请注意 pgen程序运行重新生成 Include/graminit.hPython/graminit.c ,然后重新编译几个文件。

修改AST生成代码

在 Python 解析器创建解析树后,这棵树被转换为 AST,因为 AST 是 much simpler to work with在编译过程的后续阶段。

所以,我们要访问 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 )是另一个使用迷你语言(换句话说,DSL)来简化编程的 Python 源代码示例。另请注意,由于 Parser/asdl_c.py是一个Python脚本,这是一种 bootstrapping - 要从头开始构建 Python,Python 必须可用。

虽然 Parser/asdl_c.py生成了管理我们新定义的 AST 节点的代码(到文件 Include/Python-ast.hPython/Python-ast.c 中),我们仍然需要手动编写将相关解析树节点转换为它的代码。这是在文件 Python/ast.c 中完成的.在那里,一个名为 ast_for_stmt 的函数将语句的解析树节点转换为 AST 节点。再次在老 friend 的指导下 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_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 的同时编码的。 ,区别在于 until我决定不支持 else条款。正如预期的那样,AST 是递归创建的,使用其他 AST 创建函数,如 ast_for_expr用于条件表达式和 ast_for_suiteuntil 的 body 陈述。最后,一个名为 Until 的新节点被退回。

请注意,我们访问了解析树节点 n使用一些宏,如 NCHCHILD .这些都值得理解——他们的代码在 Include/node.h .

题外话:AST 组合

我选择为 until 创建一种新类型的 AST声明,但实际上这不是必需的。我本可以节省一些工作并使用现有 AST 节点的组合来实现新功能,因为:
until condition:
# do stuff

在功能上等同于:
while not condition:
# do stuff

而不是创建 Until节点在 ast_for_until_stmt ,我本来可以创建一个 Not带有 While 的节点作为 child 的节点。由于 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也就是说,它是一个从 AST 定义文件自动生成到 _stmt_kind 中的常量(实际上是 Include/Python-ast.h 枚举的值)。 .不管怎样,我们打电话 compiler_until当然,这仍然不存在。我一会儿说。

如果你和我一样好奇,你会注意到 compiler_visit_stmt是奇特的。没有金额 grep -ping 源树显示它被调用的位置。在这种情况下,只剩下一个选项 - C macro-fu。事实上,一个简短的调查让我们找到了 VISITPython/compile.c 中定义的宏:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \

它用于调用 compiler_visit_stmtcompiler_body .回到我们的业务,但是...

正如所 promise 的,这里是 compiler_until :
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 字节码的深入理解而编写的。像文章的其余部分一样,它是模仿亲属完成的 compiler_while功能。然而,通过仔细阅读,请记住 Python VM 是基于堆栈的,并查看 dis 的文档。模块,其中有 a list of Python bytecodes通过描述,可以理解发生了什么。

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

进行所有更改并运行后 make ,我们可以运行新编译的 Python 并尝试我们的新 until声明:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1

瞧,它有效!让我们看看使用 dis 为新语句创建的字节码模块如下:
import dis

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

dis.dis(myfoo)

结果如下:
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:如果条件为真,我们跳转到循环之后。这是 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_BuildPyAST_Compile调用符号表模块 ( Python/symtable.c ),它以类似于代码生成函数的方式遍历 AST。每个作用域都有一个符号表有助于编译器找出一些关键信息,例如哪些变量是全局变量,哪些是作用域的局部变量。

为了解决这个问题,我们必须修改 symtable_visit_stmt函数在 Python/symtable.c , 添加处理代码 until语句,在 while 的类似代码之后声明 [3] :
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 symtable_visit_stmt的switch语句中没有处理枚举值并提示。检查编译器警告总是很重要的!

现在我们真的完成了。在此更改后编译源代码会执行 myfoo(3)按预期工作。

结论

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

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

引用

我使用了一些优秀的引用资料来构建这篇文章。它们在这里,没有特别的顺序:
  • PEP 339: Design of the CPython compiler - 可能是 Python 编译器最重要和最全面的官方文档。很短,它痛苦地显示了 Python 内部良好文档的稀缺性。
  • “Python 编译器内部” - Thomas Lee 的文章
  • “Python:设计与实现”——Guido van Rossum 的演讲
  • Python (2.5) 虚拟机,导览 - Peter Tröger 的演讲

  • original source

    关于python - 你能在 Python 的语法中添加新的语句吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/214881/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com