你可以添加新的语句到Python的语法?

你可以添加新的语句(如printraisewith )到Python的语法吗?

说,让…

 mystatement "Something" 

要么,

 new_if True: print "example" 

不是如果你应该 ,而是如果有可能(短的修改python解释器代码)

您可能会发现这很有用 – Python的内部:向Python添加一个新的语句 ,在这里引用:


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

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

until声明

有些语言,比如Ruby,有一个until语句,它是whileuntil num == 0等于while num != 0 )的补语。 在Ruby中,我可以这样写:

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

它会打印:

 3 2 1 

所以,我想为Python添加一个类似的function。 也就是说,可以写:

 num = 3 until num == 0: print(num) num -= 1 

语言宣传的题外话

本文不试图向Python添加一个until语句。 虽然我认为这样的陈述会使代码更加清晰,而且这篇文章展示了如何添加容易,我完全尊重Python的极简主义哲学。 我所要做的,实际上是深入了解Python的内部工作。

修改语法

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

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

必须对语法文件进行两处修改。 首先是为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] :这演示了在修改我不熟悉的源代码时使用的一种常用技术: 按相似性工作 。 这个原则并不能解决你所有的问题,但肯定可以缓解这个过程。 因为所有必须要做的事情也必须要完成,这是一个非常好的指导方针。

请注意,我已经决定从我的定义中排除else子句,只是为了使它有点不同(因为坦率地说,我不喜欢循环的else子句,不认为它适合于Python的Zen) 。

第二个更改是修改compound_stmt的规则以包含until_stmt ,如上面的代码片段所示。 再次,在while_stmt之后。

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

修改AST生成代码

在Pythonparsing器创build了一个parsing树后,这棵树被转换成一个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脚本,这是一种自举 – 从头开始​​构buildPython,Python已经可用了。

虽然Parser/asdl_c.py生成代码来pipe理我们新定义的AST节点( Include/Python-ast.h文件Include/Python-ast.hPython/Python-ast.c ),但我们仍然需要编写代码来转换相关的分析 -树节点手动。 这在Python/ast.c文件中完成。 在那里,名为ast_for_stmt的函数将语法的parsing树节点转换为AST节点。 再次,在我们的老朋友的指导下,我们直接进入处理复合语句的大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是recursion创build的,使用其他的AST创build函数,如ast_for_expr用于条件expression式, ast_for_suite用于until语句的主体。 最后,返回一个名为Until的新节点。

请注意,我们使用NCHCHILD等macros来访问分析树节点n 。 这些值得理解 – 他们的代码在Include/node.h

离题:AST组成

我select为until语句创build一个新types的AST,但实际上这不是必需的。 我可以保存一些工作,并使用现有AST节点的组合来实现新function,因为:

 until condition: # do stuff 

在function上等同于:

 while not condition: # do stuff 

而不是在ast_for_until_stmt中创buildUntil节点,我可以创build一个具有While节点的Not节点作为子节点。 由于AST编译器已经知道如何处理这些节点,因此可以跳过该过程的后续步骤。

将AST编译成字节码

下一步是将AST编译成Python字节码。 编译有一个中间结果,这是一个CFG(控制stream图),但由于相同的代码处理它,我现在将忽略这个细节,离开它的另一篇文章。

我们接下来看的代码是Python/compile.c 。 继while ,我们find函数compiler_visit_stmt ,它负责将语句编译成字节码。 我们添加一个Until

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

如果您想知道Until_kind是什么,那么它是一个从AST定义文件自动生成的Include/Python-ast.h常量(实际上是_stmt_kind枚举的值)。 无论如何,我们调用compiler_until ,当然,它仍然不存在。 我会花一点时间。

如果你像我一样好奇,你会注意到compiler_visit_stmt是奇特的。 grep -ping源树的数量不会显示它被调用的地方。 当这种情况下,只剩下一个选项 – Cmacros – 傅。 事实上,一个简短的调查使我们看到了在Python/compile.c定义的VISITmacros:

 #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虚拟机是基于堆栈的,并且浏览dis模块的文档,该模块具有带描述的Python字节码列表,可以理解发生了什么。

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

在完成所有更改并运行make ,我们可以运行新编译的Python并尝试使用新的until语句:

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

瞧,它的工作! 让我们通过使用dis模块来查看为新语句创build的字节码,如下所示:

 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' 

哇…这不可能是好事。 那么出了什么问题?

缺less符号表的情况

Python编译器在编译AST时执行的其中一个步骤是为编译的代码创build一个符号表。 在PySymtable_Build中对PyAST_Compile调用调用符号表模块( Python/symtable.c ),它以类似于代码生成函数的方式来引导AST。 每个范围都有一个符号表可以帮助编译器找出一些关键信息,比如哪些variables是全局variables,哪些variables是范围本地的。

为了解决这个问题,我们必须修改Python/symtable.csymtable_visit_stmt函数,在while语句的类似代码之后添加代码以处理until语句[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添加新的语句。 尽pipe在Python编译器的代码中需要相当多的修改,但是这个改变并不难实现,因为我使用了类似的和现有的语句作为指导。

Python编译器是一个复杂的软件,我不认为它是一个专家。 不过,我真的对Python的内部感兴趣,尤其是它的前端。 因此,我发现这个练习是编译器原理和源代码理论研究的一个非常有用的伴侣。 它将成为未来文章的基础,将深入到编译器中。

参考

我用了一些很好的参考文献来构build这篇文章。 在这里他们没有特别的顺序:

  • PEP 339:CPython编译器的devise – 可能是Python编译器最重要和最全面的官方文档。 很短,它痛苦地显示了Python内部的良好文档的稀缺性。
  • “Python编译器内部” – Thomas Lee撰写的一篇文章
  • “Python:devise与实现” – Guido van Rossum的介绍
  • Python(2.5)虚拟机,导游 – PeterTröger介绍

原始来源

一种做这种事情的方法是对源进行预处理并对其进行修改,将添加的语句翻译成python。 这种方法会带来各种各样的问题,我不会推荐它用于一般用途,但是对于语言实验或特定目的元编程,它偶尔会是有用的。

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

 myprint "This gets logged to file" 

将相当于

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

关于如何进行replace,从正则expression式replace到生成一个AST,到编写你自己的parsing器有多种select,取决于你的语法与现有的python匹配的程度。 一个好的中间方法是使用tokenizer模块。 这应该允许你添加新的关键字,控制结构等,同时解释源代码类似于python解释器,从而避免粗糙正则expression式解决scheme会导致破损。 对于上面的“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成为关键字,所以在其他地方作为variables可能会导致问题)

那么问题是如何使用它,以便您的代码可以从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

另一个相当整洁(尽pipe是hacky)的解决scheme是创build一个自定义的编码(见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预处理器,那么您可能会很熟悉。 主要的是debugging。 所有python看到的是预处理文件,这意味着在堆栈跟踪等打印的文本将参考。 如果您已经进行了大量的翻译,这可能与您的原文有很大不同。 上面的例子并没有改变行号等,所以不会太不同,但是改变的越多,就越难以弄清楚。

是的,在一定程度上是可能的。 有一个模块使用sys.settrace()来实现gotocomefrom “关键字”:

 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改变版本,你需要非常小心,不要引入错误。

但是,我不知道你为什么想要。 Python的面向对象特性使得用语言实现类似的结果非常简单。

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

更具体的答案:安装EasyExtend ,并通过以下步骤

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

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

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

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非terminal是Python语法的一部分,它是新语句所在的地方。parsing器现在将识别新语句,即包含它的源文件将被parsing。 编译器会拒绝它,因为它仍然必须转换成有效的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并input

 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编程来定义像infix操作符或小语句这样的方便的东西。 对于更复杂的东西,比如通过构build一个langlet来embedded整个语言,就没有办法用完整的语法方法。

这是一个非常简单但却很糟糕的方式来添加新的语句, 只在解释模式 。 我正在使用它只用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 >>> tt command: ['t', 't'] this is a test ignoring ('t',) >>> ^D 

我已经find了关于添加新的语句的指南,由Google将其从PDF转换为HTML:

http://209.85.173.104/search?q=cache:IjUb82taSq0J:www.troeger.eu/teaching/pythonvm08lab.pdf+python+add+statement&hl=en&ct=clnk&cd=10

基本上,为了添加新的语句,您必须编辑Python/ast.c (除其他外)并重新编译python二进制文件。

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

使用EasyExtend可以做到这一点 :

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

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

有一种基于python的语言叫做Logix ,你可以用它来做这样的事情。 它还没有开发一段时间,但你要求的function与最新版本一起工作。

这不是完全增加语言语法的新语句,但macros是一个强大的工具: https : //github.com/lihaoyi/macropy

有些东西可以用装饰器来完成。 让我们假设,Python没有声明。 然后,我们可以实现类似的行为:

 # ====== 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()) 

然而,这是一个相当不干净的解决scheme。 特别是装饰器调用函数并将_设置为None是意外的。 澄清:这个装饰器相当于写作

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

而装饰者通常希望修改而不是执行function。

我之前在一个脚本中使用了这样一个方法,在这个脚本中,我不得不暂时设置几个函数的工作目录。

十年前你不能,我怀疑这是变了。 但是,如果您准备重新编译python,那么修改语法并不困难,我也怀疑是否更改过。