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

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

说,允许. 。

mystatement "Something"

或者,

new_if True:
print "example"

如果使用 应该,则不会这样,但如果可能的话(除非修改 python 解释器代码)

34568 次浏览

十年前你做不到,现在也一样。然而,如果您准备重新编译 python,那么在当时修改语法并不是很困难,我怀疑这也没有改变。

除了更改和重新编译源代码(在开放源代码中 是可能的)之外,更改基本语言实际上是不可能的。

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

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

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

我找到了一个增加新陈述的指南:

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

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

有可能的话,不要。您可以通过函数和类实现几乎所有的功能(这不需要人们仅仅为了运行您的脚本而重新编译 python。.)

这样做的一种方法是对源代码进行预处理和修改,将添加的语句转换为 python。这种方法会带来各种各样的问题,我不推荐将其用于通用用途,但是对于语言实验或特定用途的元编程,它偶尔会很有用。

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

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 中使用。一种方法是编写自己的导入函数,并使用它来加载用自定义语言编写的代码。即:

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 模块

另一个相当简洁(尽管有些粗糙)的解决方案是创建一个自定义编码(参见 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)以注释“ # code: 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 看到的都是预处理文件,这意味着打印在堆栈跟踪中的文本等将引用该文件。如果您已经执行了重要的翻译,那么这可能与您的源文本非常不同。上面的例子不会改变行号等,所以不会有太大的不同,但是你改变得越多,就越难找出来。

过时 :
根据 Logix 网站,Logix 项目现在已经废弃,不再开发。

有一种基于 python 的语言叫做 Logix,你可以用它来做这些事情。它已经有一段时间没有开发了,但是您要求 工作提供的最新版本的特性。

是的,在某种程度上是可能的。有一个 模组使用 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"

可以使用 易扩展来完成这项工作:

EasyExtended (EE)是一个预处理器 生成器和元编程 使用纯 Python 和 与 CPython 集成 EasyExtended 的目的是创建 扩展语言 Python 的自定义语法和语义。

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

更具体的答案是: 安装 易扩展,然后执行以下步骤

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

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

如果没有额外的规范,那么将在 EasyExtended/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 转换为 langlet/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 可以让用户不用关心语法就可以定义简单的事情。但是电子工程学是非常可靠的模块化的一些错误。因此,API 的出现只是时间问题,它允许程序员使用方便的面向对象编程来定义方便的东西,比如中缀操作符或小语句。对于更复杂的事情,比如通过构建一个 anglet 在 Python 中嵌入整个语言,没有办法使用完整的语法方法。

这里有一个非常简单但很糟糕的添加新语句的方法,只能用解释模式。我只使用 sys.displayhook 编辑基因注释的小单字母命令,但为了回答这个问题,我还为语法错误添加了 sys.excthook。后者非常难看,从 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


你可能会发现这个很有用-Python 内部: 向 Python 添加新语句,引用如下:


本文试图更好地理解 Python 的前端是如何工作的。仅仅阅读文档和源代码可能有点枯燥,因此我在这里采用了一种亲身实践的方法: 我将向 Python 添加一条 until语句。

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

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

一个语言倡导的题外话

本文并不建议在 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:

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也必须做,所以它可以作为一个相当好的指导方针。

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

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

在修改 Grammar/Grammar之后运行 make时,请注意运行 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 创建函数,比如用于条件表达式的 ast_for_expr和用于 until语句体的 ast_for_suite。最后,返回一个名为 Until的新节点。

注意,我们使用一些宏(如 NCHCHILD)访问解析树节点 n。这些是值得理解的-他们的代码是在 Include/node.h

题外话: AST 成分

我选择为 until语句创建一个新类型的 AST,但实际上这是不必要的。我本可以节省一些工作,并使用现有 AST 节点的组合来实现新功能,因为:

until condition:
# do stuff

在功能上等同于:

while not condition:
# do stuff

与在 ast_for_until_stmt中创建 Until节点不同,我可以将 While节点作为子节点创建一个 Not节点。由于 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是什么,它是从 AST 定义文件自动生成到 Include/Python-ast.h的一个常量(实际上是 _stmt_kind枚举的值)。无论如何,我们称之为 compiler_until,当然,仍然不存在。我一会儿再说。

如果你像我一样好奇,你会注意到 compiler_visit_stmt是奇特的。对源树进行多少 grep-ping 都不能显示它被调用的位置。在这种情况下,只剩下一个选项-C 宏-fu。事实上,经过简短的调查,我们找到了 Python/compile.c定义的 VISIT宏观:

#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \

它用于在 compiler_body中调用 compiler_visit_stmt

正如所承诺的,这里是 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 时执行的步骤之一是为编译的代码创建一个符号表。PyAST_Compile中对 PySymtable_Build的调用调用符号表模块(Python/symtable.c) ,该模块以类似于代码生成函数的方式遍历 AST。为每个范围提供一个符号表可以帮助编译器找出一些关键信息,比如哪些变量是全局的,哪些是范围的局部变量。

为了解决这个问题,我们必须修改 Python/symtable.c中的 symtable_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 添加新语句。尽管需要对 Python 编译器的代码进行大量修改,但是这个改变并不难实现,因为我使用了一个类似的现有语句作为指导原则。

Python 编译器是一个复杂的软件块,我并不认为自己是这方面的专家。然而,我真的对 Python 的内部结构感兴趣,尤其是它的前端。因此,我发现这个练习对于编译器原理和源代码的理论研究非常有用。它将作为将来深入编译器的文章的基础。

参考文献

我用了一些很好的参考资料来构建这篇文章,以下是它们,没有特定的顺序:

  • PEP 339: CPython 编译器的设计 ——可能是 Python 编译器最重要和最全面的 正式的文档。由于篇幅很短,它痛苦地显示了 Python 内部良好文档的稀缺性。
  • “ Python 编译器内部”—— Thomas Lee 的一篇文章
  • “ Python: 设计与实现”-吉多·范罗苏姆讲座
  • Python (2.5)虚拟机,导游-Peter Tröger 演讲

原始来源

有些事情可以通过室内设计师来完成。假设 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