Как написать шаблон посетителя для абстрактного синтаксического дерева в Python?

мой коллега предложил мне написать шаблон посетителя для навигации по AST. Может ли кто-нибудь сказать мне больше, как я начну писать?

насколько я понимаю, каждый узел в AST будет иметь visit() способ (?) это как-то называется (откуда?). На этом мое понимание заканчивается.

чтобы упростить все, предположим, что у меня есть узлы Root, Expression, Number, Op и дерево выглядит так:

       Root
        |
       Op(+)
      /   
     /     
 Number(5)  
             Op(*)
             /   
            /     
           /       
       Number(2)   Number(444)

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

 5 + 2 * 444

Спасибо, Бодя Cydo.

3 ответов


Википедия имеет отличный обзор как работает шаблон посетителя, хотя пример реализации, который они используют в Java. Вы можете легко перенести это на Python, хотя, нет?

в принципе, вы хотите реализовать механизм двойной диспетчеризации. Каждый узел в вашем AST должен будет реализовать accept() метод (не visit() метод). Метод принимает в качестве аргумента объект посетителя. В реализации этого accept() метод, вы называете а visit() метод объекта посетителя (будет один для каждого типа узла AST; в Java вы будете использовать перегрузку параметров, в Python, я полагаю, вы можете использовать разные visit_*() методов). Правильный посетитель будет отправлен с правильным типом узла в качестве аргумента.


посмотреть документы на ast.NodeVisitor, например, грубая возможность может быть:

import ast

class MyVisitor(ast.NodeVisitor):
  def visit_BinaryOp(self, node):
    self.visit(node.left)
    print node.op,
    self.visit(node.right)
  def visit_Num(self, node):
    print node.n,

конечно, это не выделяет скобки даже там, где это необходимо, и т. д., Поэтому на самом деле больше работы сделано, но это начало;-).


два варианта реализации шаблона посетителя в Python, с которыми я столкнулся в Интернете чаще всего:

  • один к одному перевод примера из книги Desigh Patterns by Gamma et al.
  • использование дополнительных модулей для двойной отправки

переведенный пример из книги шаблонов Desigh

этот вариант использует accept() методы в классах структуры данных и соответствующие visit_Type() методы в посетители.

структуры данных

class Operation(object):
    def __init__(self, op, arg1, arg2):
        self.op = op
        self.arg1 = arg1
        self.arg2 = arg2
    def accept(self, visitor):
        visitor.visitOperation(self)

class Integer(object):
    def __init__(self, num):
        self.num = num
    def accept(self, visitor):
        visitor.visitInteger(self)

class Float(object):
    def __init__(self, num):
        self.num = num
    def accept(self, visitor):
        visitor.visitFloat(self)

expression = Operation('+', Integer('5'),
                            Operation('*', Integer('2'), Float('444.1')))

Infix Печать Посетитель

class InfixPrintVisitor(object):
    def __init__(self):
        self.expression_string = ''
    def visitOperation(self, operation):
        operation.arg1.accept(self)
        self.expression_string += ' ' + operation.op + ' '
        operation.arg2.accept(self)
    def visitInteger(self, number):
        self.expression_string += number.num
    def visitFloat(self, number):
        self.expression_string += number.num

Префикс Печати Посетитель

class PrefixPrintVisitor(object):
    def __init__(self):
        self.expression_string = ''
    def visitOperation(self, operation):
        self.expression_string  += operation.op + ' '
        operation.arg1.accept(self)
        self.expression_string  += ' '
        operation.arg2.accept(self)
    def visitInteger(self, number):
        self.expression_string += number.num
    def visitFloat(self, number):
        self.expression_string += number.num

тест

infixPrintVisitor = InfixPrintVisitor()
expression.accept(infixPrintVisitor)
print(infixPrintVisitor.expression_string)
prefixPrintVisitor = PrefixPrintVisitor()
expression.accept(prefixPrintVisitor)
print(prefixPrintVisitor.expression_string)

выход

5 + 2 * 444.1
+ 5 * 2 444.1

использование дополнительных модулей

этот вариант использует @functools.singledispatch() декоратор (доступен в стандартной библиотеке Python начиная с Python В3.4).

структуры данных

class Operation(object):
    def __init__(self, op, arg1, arg2):
        self.op = op
        self.arg1 = arg1
        self.arg2 = arg2

class Integer(object):
    def __init__(self, num):
        self.num = num

class Float(object):
    def __init__(self, num):
        self.num = num

expression = Operation('+', Integer('5'), 
                            Operation('*', Integer('2'), Float('444.1')))

Infix Печать Посетитель

from functools import singledispatch

@singledispatch
def visitor_print_infix(obj):
    pass
@visitor_print_infix.register(Operation)
def __(operation):
    return visitor_print_infix(operation.arg1) + ' ' \
               + operation.op + ' ' \
               + visitor_print_infix(operation.arg2)
@visitor_print_infix.register(Integer)
@visitor_print_infix.register(Float)
def __(number):
    return number.num

Префикс Печати Посетитель

from functools import singledispatch

@singledispatch
def visitor_print_prefix(obj):
    pass
@visitor_print_prefix.register(Operation)
def __(operation):
    return operation.op + ' ' \
               + visitor_print_prefix(operation.arg1) + ' ' \
               + visitor_print_prefix(operation.arg2)
@visitor_print_prefix.register(Integer)
@visitor_print_prefix.register(Float)
def __(number):
    return number.num

тест

print(visitor_print_infix(expression))
print(visitor_print_prefix(expression))

выход

5 + 2 * 444.1
+ 5 * 2 444.1

причина, по которой я предпочитаю этот вариант заключается в том, что он исключает accept() методы и полностью отделяет структуру данных от операций, реализованных в посетители. Расширение структуры данных новыми элементами не требует изменения посетителей. Посетители по умолчанию игнорируют неизвестные типы элементов (см. определения с помощью pass ключевое слово). Недостатком этого метода является то, что singledispatch декоратор не может использоваться с методами экземпляра напрямую, хотя, есть способы заставить его работать.

для Python перед v3.4 мультиметоды модуль может использоваться аналогично декоратору singledispatch. Одним из недостатков модуля multimethods является то, что метод посетителя, применяемый к заданному элементу структуры данных, выбирается не только на основе типа элемента, но и порядка, в котором объявляются методы. Сохранение определений методов в правильном порядке может быть громоздким и подверженным ошибкам для структур данных со сложной иерархией наследования.