你能否向Python语法中添加新的语句?

139

你能否向Python语法中添加新的语句(例如printraisewith)?

比如说,允许……

mystatement "Something"

或者,

new_if True:
    print "example"

并不是说你必须这样做,而是如果在不修改Python解释器代码的情况下可能这样做。


12
在某种相关的情况下,可能需要动态创建新语句(而不是严格地“扩展”语言),这对于将交互式解释器用作计算器甚至操作系统 shell 的人非常方便。我经常会动态创建一些临时函数来完成我要重复执行的任务,在这些情况下,创建缩写命令如宏或语句而不是键入长名称和函数()语法会更好。当然,这并不是 Py 的真正用途,但人们确实花费了大量时间在交互式环境中使用它。 - Kilo
5
@Kilo可能值得看一下ipython - 它具有许多类似于shell的功能,例如您可以使用常规的“ls”和“cd”命令,制表符自动完成,许多宏功能等。 - dbr
有些语言非常可扩展,例如Forth和Smalltalk,但它们的语言范式与Python所使用的不同。对于这两种语言,任何新单词(Forth)或方法(Smalltalk)都成为该安装的组成部分,不可区分的一部分。因此,每个Forth或Smalltalk安装随着时间的推移成为一个独特的创作。此外,Forth是基于RPN的。但是想到DSL,Python应该可以实现这样的东西。尽管正如其他人在这里所说的那样,为什么要这样呢? - user693708
1
作为一位精通Python和Forth的人,而且曾经实现过几个Forth编译器,我可以在这里以某种权威的程度做出贡献。如果没有获得Python内部解析器的原始访问权限,那是完全不可能的。你可以通过预处理来伪造它,就像下面(坦白说,相当流畅!)的答案所示,但真正更新语言的语法和/或语义在热解释器中是不可能的。这既是Python的诅咒,也是它胜过类似Lisp和Forth的语言的优势。 - Samuel A. Falvo II
13个回答

194
你可能会发现这个链接有用 - Python internals: adding a new statement to Python,以下是引用的内容:
这篇文章试图更好地理解Python前端是如何工作的。仅仅阅读文档和源代码可能有点无聊,所以我采取了实践方法:我将添加一个"until"语句到Python中。
本文的所有编码都是针对Python Mercury 仓库镜像的最新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源文件的引用是相对于源树的根目录给出的,该目录是构建Python时运行configure和make的目录。

必须对语法文件进行两个修改。第一个是添加until语句的定义。我找到了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,它可以作为一个相当好的指导方针。

请注意,我已经决定从until的定义中排除else子句,只是为了让它有点不同(而且因为说实话,我不喜欢循环的else子句,认为它与Python之禅不太匹配)。

第二个更改是修改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 一样)是 Python 源代码使用迷你语言(换句话说,DSL)简化编程的另一个示例。还要注意,由于 Parser/asdl_c.py 是一个 Python 脚本,这是一种bootstrapping - 要从头构建 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中,我可以创建一个带有While节点的Not节点作为子节点,而不是创建Until节点。由于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宏技巧。确实,简短的调查将我们带到了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虚拟机是基于堆栈的,并且浏览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函数,添加处理until语句的代码,与处理while语句的类似的代码[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: 设计与实现" - Guido van Rossum 的演示文稿
  • Python (2.5) 虚拟机,一次导览 - Peter Tröger 的演示文稿

原始来源


10
很棒的文章(/博客),谢谢!我接受了,因为它完美地回答了问题,并且“不要那样做”/“编码:mylang”的答案已经得到了高度赞同,所以会很好地按顺序出现 \o/ - dbr
1
但不幸的是,这不是一个答案。链接的文章是答案,但你不能点赞或接受它。完全由链接组成的答案是不被鼓励的。 - Alfe
6
@Alfe: 这篇帖子是两年前发布的,被16位读者接受和点赞。请注意,它链接到我的博客文章,将大篇幅文章复制到StackOverflow并不是我的意图。如果您觉得有用,请随意进行编辑,而不是充当警察角色。 - Eli Bendersky
3
@EliBendersky 的文章用“有用”这个词可能有点轻描淡写。感谢你详细解释了这些在 Python 中实际运作的事情。这确实帮助我理解了 AST,而这对我的当前工作非常相关。如果你好奇的话,我的版本中的 untilisa/isan,例如 if something isa dict:if something isan int: - Inversus
8
好的,这个答案是“从Python源代码中编写和编译自己的语言分支”。 - ThorSummoner
显示剩余2条评论

59

有一种方法可以做到这样的事情,那就是预处理源代码并进行修改,将您添加的语句转换成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函数,并使用它来加载用您的自定义语言编写的代码。例如:

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看到的只是预处理后的文件,这意味着在堆栈跟踪等文本打印中所引用的内容可能与源文本大不相同。如果你进行了重要的翻译,则可能会非常不同。上面的示例并没有更改行号等内容,因此不会有太大差异,但是你进行的更改越多,弄清楚问题就越困难。


14
好的!与其说“不能做到”,你实际上给出了几个好答案(总结起来就是“你真的不想这样做”)。点赞。 - c0m4
我不确定我理解第一个示例的工作原理 - 尝试在一个仅包含print 1作为唯一代码行的模块上使用myimport会产生=1 ... SyntaxError: invalid syntax - olamundo
@noam:不确定你遇到了什么问题——在这里,我得到了预期的“1”输出。(这是将上面的两个块“import tokenize”和“import new”放入文件a.py中,以及“b=myimport("b.py")”,并且b.py只包含“print 1”所做的操作)。是否还有其他错误(堆栈跟踪等)? - Brian
3
Python3 似乎不允许这样做,虽然并非有意而为之;我遇到了一个 BOM 错误。 - Tobu
请注意,import 使用内置的 __import__,因此如果您在导入需要修改的模块之前覆盖了它,您就不需要单独使用 myimport - Tobias Kienzler
codecs.StreamReader.__init__(self, *args, **kwargs) 应该改为 super(StreamReader, self).__init__(self, *args, **kwargs),对吗? - Tobias Kienzler

21

是的,在某种程度上是可能的。有一个模块利用 sys.settrace() 实现了gotocomefrom“关键字”:module

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"

4
这并不是真正的新语法,只是看起来像而已。 - Hans Nowak
5
这段文字的标题是:“‘goto’模块是2004年4月1日愚人节玩笑。是的,它可以工作,但仍然是个玩笑。请不要在实际代码中使用它!” - Jim
11
@Jim可能会重新考虑-1,因为它提示了你关于实现机制的信息。这是一个不错的起点。 - n611x007

14

除非更改并重新编译源代码(这在开源中是可能的),否则不太可能更改基础语言。

即使您重新编译源代码,也不会变成Python,只是您需要非常小心地避免引入错误的修改版本。

然而,我不确定为什么您想要这样做。Python的面向对象特性使得使用当前语言实现类似的结果非常简单。


2
我在一个观点上持不同意见。如果你添加新的关键字,我认为它仍然是Python。如果你更改现有的关键字,那就像你所说的那样是被篡改过的。 - Bill the Lizard
11
如果添加新的关键词,它将成为一种Python衍生语言。如果更改关键词,它将成为一种不兼容Python的语言。 - tzot
1
如果你添加关键字,可能会忽略“简单易学的语法”和“丰富的库”的重点。我认为语言特性几乎总是一个错误(例如COBOL、Perl和PHP)。 - S.Lott
5
新的关键字会破坏使用它们作为标识符的 Python 代码。 - akaihola

13
一般回答:您需要预处理源文件。
更具体的回答:安装EasyExtend,并按照以下步骤进行操作:
i)创建一个新的语言扩展(extension language)。
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) 进入 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这样的更复杂的事情,没有绕过完整语法方法的办法。


12

这是一种非常简单但很差的方法,仅在解释模式下才能添加新语句。我正在使用它来为编辑基因注释的小型1字母命令提供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


4
我找到了一份添加新语句的指南:https://troeger.eu/files/teaching/pythonvm08lab.pdf。基本上,要添加新语句,必须编辑Python/ast.c(以及其他内容),然后重新编译Python二进制文件。虽然这是可能的,但不建议这样做。你可以通过函数和类实现几乎所有功能(这不需要人们重新编译Python才能运行你的脚本...)。

PDF的真实链接 - “autonversion”已经破损了,而且已经很长时间了:https://troeger.eu/files/teaching/pythonvm08lab.pdf - ZXX

3

3

使用 EasyExtend 可以实现这一点:

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


1
现在点击那个链接会跳转到一个页面:“EasyExtend已经停止开发。对于那些对EE感兴趣的人,可以去看看它的后继项目叫做Langscape。虽然名称不同,但是同样具有完整的重新设计,共同成长之旅。”由于这个信息页面可能需要关闭,因此更新答案可能是个好主意。 - celtschk

2

装饰器可以实现一些功能。比如,假设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.

装饰器通常被期望用于修改函数,而非执行函数。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接