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

发布于 2021-02-02 23:12:52

你可以添加新的语句(例如print,raise,with)Python的语法?

说,允许

mystatement "Something"

要么,

new_if True:
    print "example"

如果你应该的话,并没有那么多,但是,如果可能的话,就不那么多了(只需修改python解释器代码)

关注者
0
被浏览
65
1 个回答
  • 面试哥
    面试哥 2021-02-02
    为面试而生,有面试问题,就找面试哥。

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

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

    该until声明

    有些语言,如红宝石,有一个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
    

    语言倡导题外话

    本文并非试图建议在untilPython中添加一条语句。尽管我认为这样的声明可以使一些代码更清晰,并且本文显示了添加的难易程度,但我完全尊重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_stmtinclude until_stmt,如你在上面的代码段中所见。紧接着while_stmt又是。

    当你运行make修改后Grammar/Grammar,通知该pgen程序运行重新生成Include/graminit.hPython/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)是使用迷你语言(换句话说就是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_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 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模块的文档,该文档包含带说明的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:如果条件为true,则在循环之后跳转至。这是的正确语义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_Buildin 的调用将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的内部结构特别是前端非常感兴趣。因此,我发现此练习对于编译器原理和源代码的理论研究非常有用。它将作为以后将深入编译器的文章的基础。



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看