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

说,允许..

mystatement "Something"

或者,

new_if True:
    print "example"

没那么多,如果你 应该, ,而是如果可能的话(无需修改 python 解释器代码)

有帮助吗?

解决方案

您可能会发现这很有用 - Python 内部结构:向 Python 添加新语句, ,此处引用:


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

本文的所有编码都是针对最先进的 Py3k 分支完成的 Python Mercurial 存储库镜像.

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 的禅宗)。

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

当你跑步时 make 修改后 Grammar/Grammar, ,请注意 pgen 程序运行重新生成 Include/graminit.hPython/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 成分

我选择创建一种新类型的 AST until 声明,但实际上这是没有必要的。我可以节省一些工作并使用现有 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_stmtcompiler_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 个操作:如果条件为真,我们跳转到循环后面。这是正确的语义 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 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) 虚拟机,导游 - Peter Tröger 的演讲

原始来源

其他提示

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

例如,假设我们要引入一个“myprint”。声明,而不是打印到屏幕而不是登录到特定的文件。即:

myprint "This gets logged to file"

等同于

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

有关如何进行替换的各种选项,从正则表达式替换到生成AST,再到编写自己的解析器,具体取决于语法与现有python的匹配程度。一个好的中间方法是使用tokenizer模块。这应该允许您在解释源代码时添加新的关键字,控制结构等,类似于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中使用。一种方法是编写自己的导入函数,并使用它来加载用自定义语言编写的代码。即:

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

另一个相当简洁(尽管是hacky)的解决方案是创建自定义编码(请参阅 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看到的都是预处理文件,这意味着在堆栈跟踪等中打印的文本将引用该文件。如果您执行了重要的翻译,这可能与您的源文本有很大不同。上面的例子不会改变行号等,因此不会有太大的不同,但是你改变的越多,就越难以弄明白。

是的,在某种程度上是可能的。有一个模块,它使用 sys.settrace()来实现 goto 来自" keywords":

from goto import goto, label
for i in range(1, 10):
  for j in range(1, 20):
    print i, j
    if j == 3:
      goto .end # breaking out from nested loop
label .end
print "Finished"

如果没有更改和重新编译源代码(使用开源可能 ),则无法更改基本语言。

即使你重新编译源代码,它也不会是python,只是你的hacked-up更改版本,你需要非常小心,不要引入bug。

但是,我不确定你为什么要这样做。 Python的面向对象特性使得使用现有语言获得类似结果非常简单。

一般答案:您需要预处理源文件。

更具体的答案:安装 EasyExtend ,然后执行以下步骤

i)创建一个新的langlet(扩展语言)

import EasyExtend
EasyExtend.new_langlet("mystmts", prompt = "my> ", source_ext = "mypy")

如果没有额外的规范,应在EasyExtend / langlets / mystmts /下创建一堆文件。

ii)打开mystmts / parsedef / Grammar.ext并添加以下行

small_stmt: (expr_stmt | print_stmt  | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | exec_stmt | assert_stmt | my_stmt )

my_stmt: 'mystatement' expr

这足以定义新语句的语法。 small_stmt非终端是Python语法的一部分,它是新语句被挂接的地方。解析器现在将识别新语句,即将解析包含它的源文件。编译器会拒绝它,因为它仍然必须转换为有效的Python。

iii)现在必须添加语句的语义。为此,必须编辑  msytmts / langlet.py并添加my_stmt节点访问者。

 def call_my_stmt(expression):
     "defines behaviour for my_stmt"
     print "my stmt called with", expression

 class LangletTransformer(Transformer):
       @transform
       def my_stmt(self, node):
           _expr = find_node(node, symbol.expr)
           return any_stmt(CST_CallFunc("call_my_stmt", [_expr]))

 __publish__ = ["call_my_stmt"]

iv)cd到langlets / mystmts并输入

python run_mystmts.py

现在应该启动一个会话并且可以使用新定义的语句:

__________________________________________________________________________________

 mystmts

 On Python 2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bit (Intel)]
 __________________________________________________________________________________

 my> mystatement 40+2
 my stmt called with 42

要做出一个微不足道的声明,对吧?还没有一个API可以让人们定义简单的事情,而无需关心语法。但EE非常可靠地模拟了一些错误。因此,API的出现只是一个时间问题,它允许程序员使用简单的OO编程来定义诸如中缀运算符或小语句等方便的东西。对于更复杂的事情,例如通过构建一个langlet在Python中嵌入整个语言,无法绕过完整的语法方法。

这是一种非常简单但糟糕的方式来添加新语句,仅在解释模式下。我正在使用它来进行小的单字母命令,仅使用sys.displayhook来编辑基因注释,但我只能回答这个问题,我也为语法错误添加了sys.excepthook。后者非常难看,从readline缓冲区获取原始代码。好处是,以这种方式添加新语句非常容易。


jcomeau@intrepid:~/$ cat demo.py; ./demo.py
#!/usr/bin/python -i
'load everything needed under "package", such as package.common.normalize()'
import os, sys, readline, traceback
if __name__ == '__main__':
    class t:
        @staticmethod
        def localfunction(*args):
            print 'this is a test'
            if args:
                print 'ignoring %s' % repr(args)

    def displayhook(whatever):
        if hasattr(whatever, 'localfunction'):
            return whatever.localfunction()
        else:
            print whatever

    def excepthook(exctype, value, tb):
        if exctype is SyntaxError:
            index = readline.get_current_history_length()
            item = readline.get_history_item(index)
            command = item.split()
            print 'command:', command
            if len(command[0]) == 1:
                try:
                    eval(command[0]).localfunction(*command[1:])
                except:
                    traceback.print_exception(exctype, value, tb)
        else:
            traceback.print_exception(exctype, value, tb)

    sys.displayhook = displayhook
    sys.excepthook = excepthook
>>> t
this is a test
>>> t t
command: ['t', 't']
this is a test
ignoring ('t',)
>>> ^D

我找到了添加新语句的指南:

https://troeger.eu/files/teaching/pythonvm08lab.pdf

基本上,要添加新语句,您必须编辑 Python / ast.c (以及其他内容)并重新编译python二进制文件。

虽然有可能,但不要。你可以通过函数和类实现几乎所有东西(不需要人们重新编译python就可以运行你的脚本了。)

可以使用 EasyExtend 执行此操作:

  

EasyExtend(EE)是一个预处理器   发生器和元编程   用纯Python和Python编写的框架   与CPython集成。主要的   EasyExtend的目的是创造   扩展语言,即添加   Python的自定义语法和语义。

不是没有修改解释器。我知道过去几年中有很多语言被描述为“可扩展”,但不是你所描述的方式。您可以通过添加函数和类来扩展Python。

有一种基于python的语言,名为 Logix ,您可以使用它来执行此类操作。它暂时没有开发,但是你要求使用最新版本的功能

装饰器可以完成一些事情。我们举个例子假设,Python没有 with 语句。然后我们可以实现类似的行为:

# ====== Implementation of "mywith" decorator ======

def mywith(stream):
    def decorator(function):
        try: function(stream)
        finally: stream.close()
    return decorator

# ====== Using the decorator ======

@mywith(open("test.py","r"))
def _(infile):
    for l in infile.readlines():
        print(">>", l.rstrip())

这是一个非常不洁净的解决方案,但就像这里所做的那样。特别是装饰器调用函数并将 _ 设置为 None 的行为是意外的。澄清:这个装饰器相当于写

def _(infile): ...
_ = mywith(open(...))(_) # mywith returns None.

和装饰器通常需要修改而不是执行函数。

我之前在脚本中使用过这样的方法,我必须暂时为多个函数设置工作目录。

这并不是在语言语法中添加新语句,但宏是一个强大的工具: https:// github。 COM / lihaoyi / macropy

十年前你不能,我怀疑这种情况有所改变。但是,如果您准备重新编译python,那么修改语法并不难,我怀疑它是否已经改变了。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top