Интерпретация AST в Python 3.6: isinstance, исправление обезьян, vs. Visit_NodeType, макросы?

Предположим, что я хочу написать крошечный интерпретатор, который может вычислять выражения с бинарной операцией 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))

Теперь в 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. Используйте исправление обезьяны: например. этот пример определяет группу ???_execute методов в интерпретаторе, а затем прикрепляет их к классам, которые представляют элементы АСТ. Мне это кажется очень страшным (я не хочу знать, что произойдет, если я попытаюсь выполнить два отдельных AST с двумя разными интерпретаторами параллельно: все сломается, верно?).
  2. Определите универсальный NodeVisitor, который имеет visit_???-метод для каждого типа AST-узла, а затем выполняет некоторые диспетчеризация склейка правильного имени метода из строк и имени класса экземпляра, переданного в visit-метод. Это кажется несколько более надежным, но мне не нравится, что имена методов постоянно перестраиваются: интерпретатор должен сосредоточиться на AST, а не на создании собственного исходного кода (имен методов).
  3. Используйте дополнительные макроприспособления, которые очевидно, могут генерировать case-классы. В настоящее время я не хочу использовать какие-либо сторонние инструменты, я хочу иметь крошечный скрипт, максимально независимый от всего остального.

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

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


ИЗМЕНИТЬ

Используя 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, но не экземпляр, представляющий интерпретатор.


person Andrey Tyukin    schedule 29.03.2018    source источник
comment
Я не уверен, что вы подразумеваете под этим. Это кажется несколько более надежным, но мне не нравится, что имена методов постоянно перестраиваются: интерпретатор должен сосредоточиться на AST, а не на создании собственного исходного кода (имен методов). можешь уточнить?   -  person juanpa.arrivillaga    schedule 29.03.2018
comment
@juanpa.arrivillaga Извините, связанный пост в блоге действительно немного длинный ... Я имел в виду def visit(self, node): method_name = 'visit_' + type(node).__name__ [...] часть. Если я правильно понимаю, он создает method_name каждый раз, когда метод visit вызывается для NodeVisitor. Это, в свою очередь, означает, что простой вычислитель арифметических выражений потратит на манипуляции со строками на порядок больше времени, чем на фактические вычисления.   -  person Andrey Tyukin    schedule 29.03.2018


Ответы (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)))))
person juanpa.arrivillaga    schedule 29.03.2018
comment
Спасибо за ответ! Этот синтаксис также работает с ev.register(type(Plus), lambda p: ev(p.first) + ev(p.second))-синтаксисом, который достаточно лаконичен. Это хорошо, но, к сожалению, @singledispatch не работает, если ev является методом, так что я не могу скрыть все декораторы @singledispatch и @foobar.register в одном Interpreter-базовом классе. Так что, возможно, на самом деле было бы проще написать общий Interpreter/Fold, используя isinstance один раз, а затем расширить его в конкретных реализациях. - person Andrey Tyukin; 29.03.2018
comment
Хотя позже я узнал, что он не обладает всеми свойствами, которые мне действительно нужны, это была моя вина, потому что то, что я действительно хотел, не было должным образом отражено в моем вопросе. Может быть, я сформулирую еще один более точный вопрос с большим количеством требований. На этот вопрос, как он есть, дан удовлетворительный ответ. Еще раз спасибо. - person Andrey Tyukin; 02.04.2018