解释Python 3.6中的AST:isinstance vs. monkey-patching vs. visit_NodeType vs. macros?

Interpreting ASTs in Python 3.6: isinstance vs. monkey-patching vs. visit_NodeType vs. macros?

假设我想写一个小翻译可以用二进制运算计算表达式Plus、一元运算Negate和整数常量。

我目前只对AST的解释感兴趣,为了简单起见,让我们跳过标记化技术和解析。

在haskell中,有一种或多或少规范的方法可以做到这一点:

1
2
3
4
5
6
7
8
data Ast = Plus Ast Ast | Negate Ast | IntConst Int

ev :: Ast -> Int
ev (Plus a b) = (ev a) + (ev b)
ev (Negate x) = - (ev x)
ev (IntConst i) = i

main = print $ show $ ev $ (Plus (IntConst 50) (Negate $ IntConst 8))

现在,python 3.6似乎没有代数数据类型。我的问题是似乎有很多可能的解决办法。最明显的一个正在使用isinstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Plus:
  def __init__(self, first, second):
    self.first = first
    self.second = second

class Negate:
  def __init__(self, first):
    self.first = first

class IntConst:
  def __init__(self, value):
    self.value = value

def ev(ast):
  if isinstance(ast, Plus):
    return ev(ast.first) + ev(ast.second)
  elif isinstance(ast, Negate):
    return - ev(ast.first)
  elif isinstance(ast, IntConst):
    return ast.value

print(ev(Plus(IntConst(50), Negate(IntConst(8)))))

这可以按预期工作并打印42,但看起来有点吵。

这里还有几个选项,但每个选项都有一些缺点:

  • 使用猴子修补:例如,本例定义了一堆???_execute。方法,然后将它们附加到表示AST的元素。这个看起来很可怕对我来说(我不想知道如果我试图执行会发生什么两个独立的AST,两个不同的口译员并行:每件事都会休息,对吗?).
  • 定义一个通用的NodeVisitor。它对每种类型的ast节点都有一个visit_???方法,然后进行一些调度从字符串和类的名称中粘贴正确的方法名传递给visit方法的实例。这个看起来有点结实,但我不喜欢方法名将被永久重新生成:解释器应集中于AST,不是生成自己的源代码(方法名)。
  • 使用一些显然可以生成case类的附加宏小控件。我现在不想使用任何第三方工具,我想要一点尽可能独立于其他所有内容的脚本。
  • 我没有找到这个相关问题的答案令人满意,因为唯一的答案只是链接到一些外部工具,而且它不再被维护。

    那么,在python 3.6.x中是否有一些标准的方法来定义AST的解释程序?没有上述缺点吗?还是我应该坚持使用isinstance?或实现良好的Java风格的EDCOX1×9(不确定它是否被认为是Pythic)?

    编辑

    利用@juanpa.arrivilaga提出的functools方案,我得出了以下结论:

  • 使用collections.namedtuplefunctools.singledispatch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from collections import namedtuple
    from functools import singledispatch

    Plus = namedtuple('Plus', ['left', 'right'])
    Negate = namedtuple('Negate', ['arg'])
    IntConst = namedtuple('IntConst', ['value'])

    @singledispatch
    def ev(x): raise NotImplementedError("not exhaustive: %r" % (type(x)))
    ev.register(Plus, lambda p: ev(p.left) + ev(p.right))
    ev.register(Negate, lambda n: -ev(n.arg))
    ev.register(IntConst, lambda c: c.value)

    print(ev(Plus(IntConst(50), Negate(IntConst(8)))))

    但是,如果ev是一种方法,它似乎不起作用,因为它不能在self参数上调度(见这个相关问题),所以我只能得到一个函数ev,但没有一个实例表示交互方。


  • 如果您正在寻找更干净的代码,我认为functools.singledispatch装饰器在这种情况下可以工作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import functools

    class Plus:
      def __init__(self, first, second):
        self.first = first
        self.second = second

    class Negate:
      def __init__(self, first):
        self.first = first

    class IntConst:
      def __init__(self, value):
        self.value = value

    @functools.singledispatch
    def ev(ast):
        raise NotImplementedError('Unsupported type')

    @ev.register(Plus)
    def _(ast):
        return ev(ast.first) + ev(ast.second)

    @ev.register(Negate)
    def _(ast):
        return -ev(ast.first)

    @ev.register(IntConst)
    def _(ast):
        return ast.value

    print(ev(Plus(IntConst(50), Negate(IntConst(8)))))