你能在 Python 语法中添加新语句(比如 print
, raise
, with
)吗?
说,让..
mystatement "Something"
或者,
new_if True:
print "example"
与其说你 _应该_,不如说如果可能的话(除了修改 python 解释器代码)
原文由 dbr 发布,翻译遵循 CC BY-SA 4.0 许可协议
你能在 Python 语法中添加新语句(比如 print
, raise
, with
)吗?
说,让..
mystatement "Something"
或者,
new_if True:
print "example"
与其说你 _应该_,不如说如果可能的话(除了修改 python 解释器代码)
原文由 dbr 发布,翻译遵循 CC BY-SA 4.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 许可协议
2 回答5k 阅读✓ 已解决
2 回答1k 阅读✓ 已解决
4 回答923 阅读✓ 已解决
3 回答1.1k 阅读✓ 已解决
3 回答1.1k 阅读✓ 已解决
1 回答1.6k 阅读✓ 已解决
1 回答1.2k 阅读✓ 已解决
您可能会发现这很有用 - 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 towhile
(until num == 0
is equivalent towhile num != 0
).在 Ruby 中,我可以这样写:它将打印:
所以,我想为 Python 添加类似的功能。也就是说,能够写:
语言倡导题外话
本文不建议向 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
:[2] :这演示了我在修改我不熟悉的源代码时使用的一种常用技术: _通过相似性工作_。这个原则不会解决你所有的问题,但它绝对可以简化这个过程。由于必须为
while
完成的所有操作也必须为until
完成,因此它是一个很好的指南。请注意,我决定从我的
until
的定义中排除else
子句,只是为了让它有点不同(坦率地说,因为我不喜欢else
循环子句并且认为它不适合 Python 之禅)。第二个更改是修改
compound_stmt
的规则以包括until_stmt
,如您在上面的代码片段中所见。又是在while_stmt
之后。When you run
make
after modifyingGrammar/Grammar
, notice that thepgen
program is run to re-generateInclude/graminit.h
andPython/graminit.c
,然后重新编译几个文件。修改AST生成代码
在 Python 解析器创建了一个解析树之后,这棵树被转换成一个 AST,因为 AST 在编译过程的后续阶段 使用起来要简单得多。
因此,我们将访问
Parser/Python.asdl
它定义了 Python 的 AST 结构,并为我们的新语句添加了一个 AST 节点until
语句,再次位于while
:如果您现在运行
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.h
和Python/Python-ast.c
编写相关代码),我们仍然需要转换手动将树节点解析到其中。这是在文件Python/ast.c
中完成的。在那里,一个名为ast_for_stmt
的函数将语句的解析树节点转换为 AST 节点。再次,在我们的老朋友while
的指导下,我们直接进入大switch
处理复合语句并为until_stmt
添加一个子句现在我们应该实施
ast_for_until_stmt
。这里是:同样,这是在仔细查看等效
ast_for_while_stmt
的同时进行编码的,不同之处在于until
我决定不支持else
正如预期的那样,AST 是递归创建的,使用其他 AST 创建函数,例如ast_for_expr
用于条件表达式和ast_for_suite
until
语句的主体。最后,返回一个名为Until
的新节点。请注意,我们使用一些宏访问解析树节点
n
NCH
和CHILD
。这些值得理解——它们的代码在Include/node.h
中。题外话:AST作文
我选择为
until
语句创建一种新类型的 AST,但实际上这不是必需的。我本可以节省一些工作并使用现有 AST 节点的组合来实现新功能,因为:在功能上等同于:
Instead of creating the
Until
node inast_for_until_stmt
, I could have created aNot
node with anWhile
node as a child.由于 AST 编译器已经知道如何处理这些节点,因此可以跳过该过程的后续步骤。将 AST 编译成字节码
下一步是将 AST 编译成 Python 字节码。编译有一个中间结果,它是一个 CFG(控制流图),但由于相同的代码处理它,我暂时忽略这个细节并将其留到另一篇文章。
我们接下来要看的代码是
Python/compile.c
。继while
之后,我们找到函数compiler_visit_stmt
,它负责将语句编译成字节码。我们为Until
添加一个子句:如果您想知道
Until_kind
是什么,它是一个常量(实际上是_stmt_kind
枚举的值)从 AST 定义文件自动生成到Include/Python-ast.h
无论如何,我们调用compiler_until
当然,它仍然不存在。我会得到它的片刻。如果你像我一样好奇,你会注意到
compiler_visit_stmt
很奇怪。没有多少grep
源代码树显示它被调用的位置。出现这种情况时,只剩下一个选项——C macro-fu。事实上,一个简短的调查让我们找到了VISIT
在Python/compile.c
中定义的宏:它用于在
compiler_visit_stmt
compiler_body
。回到我们的业务,但是……正如所承诺的,这是
compiler_until
:我要坦白:这段代码并不是基于对 Python 字节码的深刻理解而编写的。与本文的其余部分一样,它是模仿 kin
compiler_while
函数完成的。然而,通过仔细阅读它,牢记 Python VM 是基于堆栈的,并浏览dis
模块的文档,其中包含带有描述 的 Python 字节码列表, 可以理解什么是继续。就是这样,我们完成了……不是吗?
完成所有更改并运行
make
后,我们可以运行新编译的 Python 并尝试我们新的until
语句:瞧,它有效!让我们看看使用
dis
模块为新语句创建的字节码,如下所示:结果如下:
最有趣的操作是数字 12:如果条件为真,我们跳转到循环之后。这是
until
的正确语义。如果不执行跳转,则循环体继续运行,直到跳回到第 35 步的条件。对我的更改感觉良好,然后我尝试运行该函数(执行
myfoo(3)
)而不是显示其字节码。结果并不令人鼓舞:哇……这可不好。那么出了什么问题呢?
缺少符号表的情况
Python 编译器在编译 AST 时执行的步骤之一是为其编译的代码创建符号表。在
PySymtable_Build
中调用PyAST_Compile
调用符号表模块(Python/symtable.c
),它在生成函数时类似于代码 AST。每个范围都有一个符号表有助于编译器找出一些关键信息,例如哪些变量是全局的,哪些是局部的。To fix the problem, we have to modify the
symtable_visit_stmt
function inPython/symtable.c
, adding code for handlinguntil
statements, after the similar code forwhile
声明 [3] :[3] :顺便说一下,如果没有此代码,编译器会警告
Python/symtable.c
。编译器注意到Until_kind
枚举值未在symtable_visit_stmt
的 switch 语句中处理并抱怨。检查编译器警告总是很重要的!现在我们真的完成了。在此更改后编译源代码会使
myfoo(3)
的执行按预期工作。结论
在本文中,我演示了如何向 Python 添加新语句。尽管需要对 Python 编译器的代码进行大量修改,但更改并不难实现,因为我使用了一个类似的现有语句作为指导。
Python 编译器是一个复杂的软件块,我并不声称自己是这方面的专家。然而,我对 Python 的内部结构非常感兴趣,尤其是它的前端。因此,我发现这个练习对于编译器原理和源代码的理论研究非常有用。它将作为以后深入了解编译器的文章的基础。
参考
我使用了一些优秀的参考资料来构建本文。它们在这里,没有特别的顺序:
原始来源