你能在 Python 的语法中添加新的语句(如print
, raise
, )吗?with
说,允许..
mystatement "Something"
或者,
new_if True:
print "example"
不是如果你应该,而是如果可能的话(没有修改python解释器代码)
你能在 Python 的语法中添加新的语句(如print
, raise
, )吗?with
说,允许..
mystatement "Something"
或者,
new_if True:
print "example"
不是如果你应该,而是如果可能的话(没有修改python解释器代码)
您可能会发现这很有用 - Python internals: added a new statement to Python,在此引用:
本文试图更好地理解 Python 前端的工作原理。仅仅阅读文档和源代码可能会有点无聊,所以我在这里采取了动手的方法:我将在until
Python 中添加一个语句。
本文的所有编码都是针对Python Mercurial 存储库镜像中的尖端 Py3k 分支完成的。
until
声明_一些语言,比如 Ruby,有一个语句,它是(等价于)until
的补充。在 Ruby 中,我可以写:while
until num == 0
while num != 0
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
),并在[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
,因此它是一个很好的指导方针。
请注意,我决定else
从我的定义中排除该子句until
,只是为了让它有点不同(坦率地说,我不喜欢else
循环子句,并且认为它不适合 Python 的禅宗)。
第二个更改是修改规则compound_stmt
to include until_stmt
,如您在上面的代码段中所见。就在 之后while_stmt
,再次。
make
修改后运行时Grammar/Grammar
,注意pgen
程序运行重新生成Include/graminit.h
和Python/graminit.c
,然后重新编译了几个文件。
在 Python 解析器创建解析树后,此树将转换为 AST,因为 AST在编译过程的后续阶段更易于使用。
因此,我们将访问它定义了 Python AST 的结构,并为我们的新语句Parser/Python.asdl
添加一个 AST 节点,同样位于以下:until
while
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
如果您现在运行make
,请注意在编译一堆文件之前,Parser/asdl_c.py
会运行以从 AST 定义文件生成 C 代码。这(如Grammar/Grammar
)是 Python 源代码使用迷你语言(换句话说,DSL)来简化编程的另一个示例。另请注意,由于Parser/asdl_c.py
是 Python 脚本,因此这是一种引导——要从头开始构建 Python,Python 必须已经可用。
虽然Parser/asdl_c.py
生成了管理我们新定义的 AST 节点的代码(到文件Include/Python-ast.h
和Python/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
类的宏访问解析树节点。这些都值得理解——他们的代码在.NCH
CHILD
Include/node.h
我选择为until
语句创建一种新类型的 AST,但实际上这不是必需的。我可以节省一些工作并使用现有 AST 节点的组合来实现新功能,因为:
until condition:
# do stuff
在功能上等同于:
while not condition:
# do stuff
我可以创建一个节点,而不是在中创建Until
节点,而是创建一个节点作为子节点。由于 AST 编译器已经知道如何处理这些节点,因此可以跳过该过程的后续步骤。ast_for_until_stmt
Not
While
下一步是将 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
in 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 字节码的深刻理解而编写的。与文章的其余部分一样,它是模仿 kincompiler_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
in的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 的内部结构非常感兴趣,尤其是它的前端。因此,我发现这个练习对于编译器原理和源代码的理论研究非常有用。它将作为将来深入了解编译器的文章的基础。
我使用了一些优秀的参考资料来构建这篇文章。它们在这里,没有特别的顺序:
做这样的事情的一种方法是预处理源并修改它,将添加的语句转换为 python。这种方法会带来各种问题,我不建议将其用于一般用途,但对于语言实验或特定用途的元编程,它有时会很有用。
例如,假设我们要引入“myprint”语句,而不是打印到屏幕上,而是记录到特定文件。IE:
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 中使用。一种方法是编写您自己的导入函数,并使用它来加载以您的自定义语言编写的代码。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 看到的是预处理文件,这意味着堆栈跟踪等中打印的文本将引用该文件。如果您进行了重要的翻译,这可能与您的源文本有很大不同。上面的例子没有改变行号等,所以不会有太大的不同,但是你改变的越多,就越难弄清楚。
是的,在某种程度上这是可能的。有一个模块用于sys.settrace()
实现goto
和comefrom
“关键字”:
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,只是您修改后的版本,您需要非常小心,不要将错误引入其中。
但是,我不确定您为什么要这样做。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 编辑基因注释的小 1 字母命令,但为了回答这个问题,我也为语法错误添加了 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 编写并与 CPython 集成的预处理器生成器和元编程框架。EasyExtend 的主要目的是创建扩展语言,即向 Python 添加自定义语法和语义。
它并不完全是在语言语法中添加新语句,但宏是一个强大的工具:https ://github.com/lihaoyi/macropy
有一种基于 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.
装饰器通常被期望修改而不是执行函数。
我之前在脚本中使用过这样的方法,我必须为几个函数临时设置工作目录。
不是不修改解释器。我知道过去几年有很多语言被描述为“可扩展的”,但不是您描述的方式。您可以通过添加函数和类来扩展 Python。
十年前你不能,我怀疑这已经改变了。但是,如果您准备重新编译python,那么修改语法并不难,我怀疑这也改变了。