当前位置:  开发笔记 > 编程语言 > 正文

你能为Python的语法添加新的语句吗?

如何解决《你能为Python的语法添加新的语句吗?》经验,为你挑选了6个好方法。

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

说,允许..

mystatement "Something"

要么,

new_if True:
    print "example"

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



1> Eli Bendersk..:

你可能会发现这很有用 - Python内部:在Python中添加一个新语句,引用到这里:


本文试图更好地理解Python的前端如何工作.只是阅读文档和源代码可能有点无聊,所以我在这里采取实践方法:我将向untilPython 添加一个语句.

本文的所有编码都是针对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

语言倡导的题外话

本文不会尝试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的Zen).

第二个更改是修改compound_stmt要包含的规则until_stmt,如上面的代码段所示.它就在之后while_stmt,再一次.

当你运行make修改后Grammar/Grammar,请注意,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的条件表达式和ast_for_suite用于身体until语句.最后,Until返回一个名为的新节点.

请注意,我们n使用NCH和和一些宏来访问parse-tree节点CHILD.这些是值得理解的 - 他们的代码在Include/node.h.

题外话:AST组成

我选择为until语句创建一种新类型的AST ,但实际上这不是必需的.我可以使用现有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的源树显示它被调用的位置.在这种情况下,只剩下一个选项 - 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_Buildin的PyAST_Compile调用调用符号表module(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的内部特别感兴趣,特别是它的前端.因此,我发现这个练习对编译器的原理和源代码的理论研究非常有用.它将作为未来文章的基础,以深入了解编译器.

参考

我使用了一些很好的参考资料来构建本文.在这里,他们没有特别的顺序:

PEP 339:CPython编译器的设计 - 可能是Python编译器最重要和最全面的官方文档.由于非常简短,它很难显示Python内部的良好文档的稀缺性.

"Python Compiler Internals" - Thomas Lee撰写的一篇文章

"Python:设计与实现" - Guido van Rossum的演讲

Python(2.5)虚拟机,导游 - PeterTröger的演讲

原始来源


优秀的文章(/博客),谢谢!接受,因为这完美地回答了这个问题,并且"不做那个"/"编码:mylang"的答案已经被高度评价,所以会很好地按顺序出现\ o /
@Alfe:这是两年前发布的,被16位读者接受并且+1.请注意,它链接到我自己的博客文章,并将大型文章复制到StackOverflow不是我打算做的事情.随意在有用的编辑中这样做,而不是打警察.
那么,这个答案是"从源代码编写和编译自己的语言,从python分叉"
@EliBendersky对于那篇文章来说,有用的是轻描淡写.感谢您解释如何在python中实际运行这些东西.这确实帮助我理解了AST,这与我目前的工作有关.**另外,如果你很好奇,我的`until`版本就是`isa` /`isan`,如果有什么东西是dict:`或`if something isan int:`

2> Brian..:

执行此类操作的一种方法是预处理源并对其进行修改,将添加的语句转换为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看到的都是预处理文件,这意味着在堆栈跟踪等中打印的文本将引用该文件.如果您执行了重要的翻译,这可能与您的源文本有很大不同.上面的例子不会改变行号等,因此不会有太大的不同,但是你改变的越多,就越难以弄明白.


好一个!你不是说'不能打'',而是给出一些好的答案(归结为'你真的不想这样做')Upvote.
Python3似乎不允许这样做,但不一定是故意的; 我收到了BOM错误.

3> Constantin..:

是的,在某种程度上它是可能的.有一个模块在那里,使用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"


这不是真正的新语法......它只是看起来像它.
@Jim可能会重新考虑-1.它暗示了你的实现机制.开始的好事.
-1:链接的页面具有以下标题:“'goto'模块是愚人节的一个玩笑,于2004年4月1日发布。是的,它是有效的,但仍然是个玩笑。请不要在真实代码中使用它!”

4> paxdiablo..:

短期改变和重新编译源代码(其中可能的开放源码),改变基本语言是不是真的有可能.

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

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


如果添加新关键字,它将是Python派生的语言.如果更改关键字,它将是一种与Python不兼容的语言.
新关键字会破坏使用它们作为标识符的Python代码.
我不同意一点.如果你*添加*新关键字,我认为它仍然是Python.如果您*更改*现有关键字,那么就像你说的那样只会被黑客攻击.

5> 小智..:

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

更具体的答案:安装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中嵌入整个语言,没有办法绕过完整的语法方法.



6> jcomeau_ictx..:

这是一种非常简单但糟糕的方式来添加新语句,仅在解释模式下.我正在使用它来进行小的单字母命令,仅使用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

推荐阅读
殉情放开那只小兔子
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有