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

新手上路,请多包涵

你能在 Python 语法中添加新语句(比如 print , raise , with )吗?

说,让..

 mystatement "Something"

或者,

 new_if True:
    print "example"

与其说你 _应该_,不如说如果可能的话(除了修改 python 解释器代码)

原文由 dbr 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 622
2 个回答

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


这篇文章试图更好地理解 Python 的前端是如何工作的。仅仅阅读文档和源代码可能有点乏味,所以我在这里采取动手实践的方法:我将向 Python 添加 until 语句。

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

until 声明

Some languages, like Ruby, have an until statement, which is the complement to while ( until num == 0 is equivalent to 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

语言倡导题外话

本文不建议向 Python 添加 until 语句。虽然我认为这样的声明会让一些代码更清晰,并且这篇文章展示了它是多么容易添加,但我完全尊重 Python 的极简主义哲学。实际上,我在这里所做的只是深入了解 Python 的内部工作原理。

修改语法

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

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

必须对语法文件进行两处修改。首先是为 until 语句添加定义。我找到了定义 while 语句的位置( while_stmt ),并在 [2] 下面添加了 until_stmt

 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 完成,因此它是一个很好的指南。

请注意,我决定从我的 until 的定义中排除 else 子句,只是为了让它有点不同(坦率地说,因为我不喜欢 else 循环子句并且认为它不适合 Python 之禅)。

第二个更改是修改 compound_stmt 的规则以包括 until_stmt ,如您在上面的代码片段中所见。又是在 while_stmt 之后。

When you run make after modifying Grammar/Grammar , notice that the pgen program is run to re-generate Include/graminit.h and Python/graminit.c ,然后重新编译几个文件。

修改AST生成代码

在 Python 解析器创建了一个解析树之后,这棵树被转换成一个 AST,因为 AST 在编译过程的后续阶段 使用起来要简单得多

因此,我们将访问 Parser/Python.asdl 它定义了 Python 的 AST 结构,并为我们的新语句添加了一个 AST 节点 until 语句,再次位于 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 脚本,这是一种 引导——要从头开始构建 Python,Python 已经可用。

虽然 Parser/asdl_c.py 生成代码来管理我们新定义的 AST 节点(进入文件 Include/Python-ast.hPython/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_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_suite until 语句的主体。最后,返回一个名为 Until 的新节点。

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

题外话:AST作文

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

 until condition:
   # do stuff

在功能上等同于:

 while not condition:
  # do stuff

Instead of creating the Until node in ast_for_until_stmt , I could have created a Not node with an While node as a 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 是什么,它是一个常量(实际上是 _stmt_kind 枚举的值)从 AST 定义文件自动生成到 Include/Python-ast.h 无论如何,我们调用 compiler_until 当然,它仍然不存在。我会得到它的片刻。

如果你像我一样好奇,你会注意到 compiler_visit_stmt 很奇怪。没有多少 grep 源代码树显示它被调用的位置。出现这种情况时,只剩下一个选项——C macro-fu。事实上,一个简短的调查让我们找到了 VISITPython/compile.c 中定义的宏:

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

它用于在 compiler_visit_stmt compiler_body 。回到我们的业务,但是……

正如所承诺的,这是 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 字节码的深刻理解而编写的。与本文的其余部分一样,它是模仿 kin compiler_while 函数完成的。然而,通过仔细阅读它,牢记 Python VM 是基于堆栈的,并浏览 dis 模块的文档,其中包含带有描述 的 Python 字节码列表, 可以理解什么是继续。

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

完成所有更改并运行 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_Build 中调用 PyAST_Compile 调用符号表模块( Python/symtable.c ),它在生成函数时类似于代码 AST。每个范围都有一个符号表有助于编译器找出一些关键信息,例如哪些变量是全局的,哪些是局部的。

To fix the problem, we have to modify the symtable_visit_stmt function in Python/symtable.c , adding code for handling until statements, after the similar code for 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:CPython 编译器的设计——可能是 Python 编译器最重要、最全面的 官方 文档。由于非常短,它痛苦地显示了 Python 内部的良好文档的稀缺性。
  • “Python 编译器内部原理”——Thomas Lee 的一篇文章
  • “Python:设计与实现”——Guido van Rossum 的演讲
  • Python (2.5) 虚拟机,导览 - Peter Tröger 的演讲

原始来源

原文由 Eli Bendersky 发布,翻译遵循 CC BY-SA 3.0 许可协议

执行此类操作的一种方法是预处理源代码并对其进行修改,将添加的语句转换为 python。这种方法会带来各种问题,我不建议将其用于一般用途,但对于语言实验或特定用途的元编程,它偶尔会有用。

例如,假设我们要引入一个“myprint”语句,而不是打印到屏幕,而是记录到一个特定的文件。 IE:

 myprint "This gets logged to file"

相当于

print >>open('/tmp/logfile.txt','a'), "This gets logged to file"

关于如何进行替换,有多种选择,从正则表达式替换到生成 AST,再到编写您自己的解析器,具体取决于您的语法与现有 python 的匹配程度。一个好的中间方法是使用分词器模块。这应该允许您添加新的关键字、控制结构等,同时以类似于 python 解释器的方式解释源代码,从而避免原始正则表达式解决方案可能导致的破损。对于上面的“myprint”,你可以编写如下转换代码:

 import tokenize

LOGFILE = '/tmp/log.txt'
def translate(readline):
    for type, name,_,_,_ in tokenize.generate_tokens(readline):
        if type ==tokenize.NAME and name =='myprint':
            yield tokenize.NAME, 'print'
            yield tokenize.OP, '>>'
            yield tokenize.NAME, "open"
            yield tokenize.OP, "("
            yield tokenize.STRING, repr(LOGFILE)
            yield tokenize.OP, ","
            yield tokenize.STRING, "'a'"
            yield tokenize.OP, ")"
            yield tokenize.OP, ","
        else:
            yield type,name

(这确实使 myprint 有效地成为关键字,因此在其他地方用作变量可能会导致问题)

那么问题是如何使用它,以便您的代码可以从 python 中使用。一种方法就是编写您自己的导入函数,并使用它来加载以您的自定义语言编写的代码。 IE:

 import new
def myimport(filename):
    mod = new.module(filename)
    f=open(filename)
    data = tokenize.untokenize(translate(f.readline))
    exec data in mod.__dict__
    return mod

然而,这要求您以不同于普通 python 模块的方式处理自定义代码。即“ some_mod = myimport("some_mod.py") ”而不是“ import some_mod

另一个相当简洁(尽管有点老套)的解决方案是创建自定义编码(参见 PEP 263 ),如 食谱所示。您可以将其实现为:

 import codecs, cStringIO, encodings
from encodings import utf_8

class StreamReader(utf_8.StreamReader):
    def __init__(self, *args, **kwargs):
        codecs.StreamReader.__init__(self, *args, **kwargs)
        data = tokenize.untokenize(translate(self.stream.readline))
        self.stream = cStringIO.StringIO(data)

def search_function(s):
    if s!='mylang': return None
    utf8=encodings.search_function('utf8') # Assume utf8 encoding
    return codecs.CodecInfo(
        name='mylang',
        encode = utf8.encode,
        decode = utf8.decode,
        incrementalencoder=utf8.incrementalencoder,
        incrementaldecoder=utf8.incrementaldecoder,
        streamreader=StreamReader,
        streamwriter=utf8.streamwriter)

codecs.register(search_function)

现在,在这段代码运行之后(例如,您可以将它放在您的 .pythonrc 或 site.py 中),任何以注释“# coding: mylang”开头的代码都将通过上述预处理步骤自动翻译。例如。

 # coding: mylang
myprint "this gets logged to file"
for i in range(10):
    myprint "so does this : ", i, "times"
myprint ("works fine" "with arbitrary" + " syntax"
  "and line continuations")

注意事项:

预处理器方法存在一些问题,如果您使用过 C 预处理器,您可能会很熟悉。最主要的是调试。所有 python 看到的都是预处理文件,这意味着打印在堆栈跟踪等中的文本将引用它。如果您进行了重要的翻译,这可能与您的源文本有很大不同。上面的例子没有改变行号等,所以不会有太大的不同,但是你改变得越多,就越难弄清楚。

原文由 Brian 发布,翻译遵循 CC BY-SA 2.5 许可协议

推荐问题