解释 Python 3.6 中的 AST:isinstance、猴子修补、visit_NodeType 与宏?

And*_*kin 5 python monkeypatching visitor-pattern algebraic-data-types python-3.x

假设我想编写一个小型解释器,可以使用二元运算 Plus、一元运算Negate和整数常量来计算表达式。

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

在 Haskell 中,有一种或多或少规范的方法来做到这一点:

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))
Run Code Online (Sandbox Code Playgroud)

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

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)))))
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 使用猴子修补:例如,这个示例在解释器中 定义了一堆???_execute 方法,然后将它们附加到表示 AST 元素的类。这对我来说看起来非常可怕(我不想知道如果我尝试使用两个不同的解释器并行执行两个单独的 AST 会发生什么:一切都会崩溃,对吧?)。
  2. 定义一个泛型,NodeVisitorvisit_???为每种类型的 AST 节点都有一个 -method,然后进行一些调度,将字符串中的正确方法名称和传递给 -method 的实例类名称粘合起来visit。这看起来更健壮,但我不喜欢永久重建方法名称:解释器应该专注于 AST,而不是生成自己的源代码(方法名称)。
  3. 使用一些显然可以生成案例类的附加宏小玩意。我目前不想使用任何第三方工具,我想要一个尽可能独立于其他所有内容的小脚本。

我没有找到令人满意的相关问题的答案 ,因为唯一的答案只是链接到一些外部工具,而且不再维护。

那么,Python 3.6.x 中是否有一些标准方法可以为没有上述缺点的 AST 定义解释器?还是我应该坚持下去isinstance?或者实现良好的旧 Java 风格Visitor(不确定它是否被认为是 pythonic)?


编辑

使用functools@juanpa.arrivilillaga的建议,我想出了以下内容:

  1. 使用collections.namedtuplefunctools.singledispatch

    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)))))
    
    Run Code Online (Sandbox Code Playgroud)

    但是,如果is a method ,它似乎不起作用ev,因为它无法在 -argument 上分派self(请参阅此相关问题),所以我只能得到一个 function ev,但没有代表解释器的实例。

jua*_*aga 2

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

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)))))
Run Code Online (Sandbox Code Playgroud)