Перевод АСЦ в Python 3.6: isinstance и обезьяна-ямочный и перейти типа и макросы?


Предположим, что я хочу написать крошечный интерпретатор. который может вычислять выражения с помощью двоичной операции Plus, унарная операция Negate и целочисленные константы.

В настоящее время меня интересует только интерпретация АСТ, поэтому давайте для простоты пропустим токенизацию и синтаксический анализ.

У Хаскелла есть более или менее канонический способ сделать это:
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:

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, как и ожидалось, но выглядит несколько шумно.

Вот еще несколько вариантов, но каждый из них имеет некоторые недостатки:
  1. используйте monkey-patching: например этот пример определяет связку ???_execute методы в интерпретаторе, а затем присоединяет их к классам, которые представляют собой элементы АСТ. Это выглядит очень страшно для меня (я не хочу знать, что произойдет, если я попытаюсь выполнить два разделите ASTs с двумя разными интерпретаторами параллельно: все будет перерыв, да?).
  2. определите общий NodeVisitor это имеет visit_??? - метод для каждого типа AST-узла, а затем выполняет некоторую диспетчеризацию склеивание правильного имени метода из строк и имени класса экземпляра, переданного в visit-метод. Это кажется несколько более надежным, но мне не нравится что имена методов перестраиваются постоянно: интерпретатор должен сосредоточиться на AST, не на генерацию собственный исходный код (имена методов).
  3. используйте некоторые дополнительные макро-Гизмо, которые , по-видимому, могут генерировать case-классы. Я в настоящее время не хочу использовать никакие сторонние инструменты, я хочу иметь крошечный маленький сценарий, который настолько независим от всего остального, насколько это возможно.

Я не нашел ответа на этот связанный с этим вопрос удовлетворительно, потому что единственный ответ просто ссылки на какой-то внешний инструмент, который, кроме того, больше не поддерживается.

Итак, есть есть какой-то стандартный способ в Python 3.6.x определить интерпретаторы для ASTs, которые не имеют ли вышеперечисленные недостатки? Или я должен просто придерживаться isinstance? Или выполнять привычные Java-стиль Visitor (не уверен, можно ли это считать обновления)?


EDIT

С использованием functools, предложенного @juanpa.arrivillaga, я придумал следующее:

  1. Используйте collections.namedtuple и functools.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)))))
    

    Однако это, кажется, не работает, если ev является метод , поскольку он не может отправлять на self-аргумент (см. этот связанный вопрос), поэтому я могу получить только функцию ev, но не экземпляр, представляющий интерпертер.

1 3

1 ответ:

Если чистый код-это то, что вы ищете, я думаю, что декоратор 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)))))