Шаблон посетителя в python

здесь приведена упрощенная реализация шаблона посетителя в С++. Можно ли реализовать что-то подобное в Python?

Мне это нужно, потому что я передам Object из кода С++ функции в Python. Моя идея состояла в том, чтобы реализовать посетителя в Python, чтобы узнать тип объекта.

Мой код на С++:

#include <iostream>
#include <string>


class t_element_base
{
public:
    virtual void accept( class t_visitor &v ) = 0;
};


class t_element_deriv_one: public t_element_base
{
public:
    void accept( t_visitor &v );

    std::string t_element_deriv_one_text()
    {
        return "t_element_deriv_one";
    }
};


class t_element_deriv_two: public t_element_base
{
public:
    void accept( t_visitor &v );

    std::string t_element_deriv_two_text()
    {
        return "t_element_deriv_one";
    }
};


class t_visitor
{
public:
    void visit( t_element_deriv_one& e ){ std::cout << e.t_element_deriv_one_text() << std::endl; }
    void visit( t_element_deriv_two& e ){ std::cout << e.t_element_deriv_two_text() << std::endl; }
};


void t_element_deriv_one::accept( t_visitor &v )
{
    v.visit( *this );
}

void t_element_deriv_two::accept( t_visitor &v )
{
    v.visit( *this );
}


int
main
(
void
)
{
    t_element_base* list[] =
    {
        new t_element_deriv_one(), new t_element_deriv_two()
    };
    t_visitor visitor;

    for( int i = 0; i < 2; i++ )
        list[ i ]->accept( visitor );
}

Ответ 1

Шаблон посетителя может быть реализован в Python, я использую его для реализации чистого интерфейса между моими данными и уровнем представления. Уровень данных может определять порядок данных. и слой презентации просто печатает/форматирует его:

В моем модуле данных я:

 class visited(object):
     ....
     def accept(self, visitor):
         visitor.visit(self)
         for child in self.children():
             child.accept(visitor)

 class typeA(visited):
    ....

Все мои классы данных наследуются от этого посещенного класса, а посещенный класс также предоставляет некоторые простые функции для базовых данных, все мои объекты нуждаются, например. имя, родительский элемент и т.д., а также методы управления дочерним списком, которые подвергаются методу children(), использованному выше. каждый из подкласс будет строить свои собственные данные, иметь свои собственные свойства и, возможно, даже свой собственный дочерний класс, которые добавляются в список детей, поддерживаемый посещенным суперклассом.

Мой класс посетителя выглядит следующим образом:

class visitor(object):
      def __init__(self, obj_id):
          data_obj = _find_data_instance( obj_id )
          data_obj.accept(self)

      def visit( self, data_obj):
          if isinstance(data_obj, typeA):
               self.visit_typeA( dataobj)

      def visit_typeA(self, dataobj):
          """Formats the data for typeA"""
          ...

_find_data_instance - это код, который создает или находит экземпляр одного из моих экземпляров данных. В моем случае все мои классы данных имеют конструктор, который принимает objectId и возвращает, а объект-посетитель знает, какой класс данных использовать.

Ответ 2

Вы можете использовать декораторы, чтобы получить то, что вы хотите. Копирование примера из этого :

class Lion: pass
class Tiger: pass
class Bear: pass

class ZooVisitor:
    @visitor(Lion)
    def visit(self, animal):
        return "Lions"

    @visitor(Tiger)
    def visit(self, animal):
        return "tigers"

    @visitor(Bear)
    def visit(self, animal):
        return "and bears, oh my!"

animals = [Lion(), Tiger(), Bear()]
visitor = ZooVisitor()
print(', '.join(visitor.visit(animal) for animal in animals))
# Prints "Lions, tigers, and bears, oh my!"

и код декоратора @visitor (в случае, если ссылка не работает):

# A couple helper functions first

def _qualname(obj):
    """Get the fully-qualified name of an object (including module)."""
    return obj.__module__ + '.' + obj.__qualname__

def _declaring_class(obj):
    """Get the name of the class that declared an object."""
    name = _qualname(obj)
    return name[:name.rfind('.')]

# Stores the actual visitor methods
_methods = {}

# Delegating visitor implementation
def _visitor_impl(self, arg):
    """Actual visitor method implementation."""
    method = _methods[(_qualname(type(self)), type(arg))]
    return method(self, arg)

# The actual @visitor decorator
def visitor(arg_type):
    """Decorator that creates a visitor method."""

    def decorator(fn):
        declaring_class = _declaring_class(fn)
        _methods[(declaring_class, arg_type)] = fn

        # Replace all decorated methods with _visitor_impl
        return _visitor_impl

    return decorator

Связанный блог (первый, похоже, уже не работает): https://chris-lamb.co.uk/posts/visitor-pattern-in-python

EDIT:

obj.__qualname__ недоступен до Python 3.3, поэтому мы должны использовать хак для более низких версий: -

def _qualname(obj):
    """Get the fully-qualified name of an object (including module)."""

    if hasattr(obj, '__qualname__'):
        qualname = obj.__qualname__
    else:
        qualname = str(obj).split(' ')[1]

    return obj.__module__ + '.' + qualname

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

Ответ 3

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

Итак, ваш приведенный выше пример может быть таким же простым, как

class C1(object):
    pass

class C2(object):
    pass

l = [C1(), C2()]
if __name__=="__main__":
    for element in l:
        print type(element)

который даст:

<class '__main__.C1'>
<class '__main__.C2'>

Ответ 4

На случай, если кто-то сочтет это полезным, я получил следующую версию @visitor answer @visitor работающую с использованием самоанализа в Python 2:

_visitors = {}

def visitor(arg_type):
    "A @visitor decorator"
    def decorated(fn):
        import inspect
        stack = inspect.currentframe()
        class_name = stack.f_back.f_code.co_name
        full_name = fn.__module__ + '.' + class_name + '.' + fn.__name__
        _visitors[(full_name, arg_type)] = fn

        def _visitor_impl(self, arg, *rest, **kwargs):
            full_name = fn.__module__ + '.' + self.__class__.__name__ + '.' + fn.__name__
            assert (full_name, arg.__class__) in _visitors, "Can't find visitor in {} for {}".format(full_name, arg.__class__.__name__)
            method = _visitors[(full_name, arg.__class__)]
            return method(self, arg, *rest, **kwargs)

        return _visitor_impl

    return decorated

Ответ 5

Прежде всего

  • что такое объектно-ориентированное программирование? это динамическая отправка по одному аргументу (неявное this в java & C++)
  • Что делает шаблон посетителя: он пытается эмулировать двойную диспетчеризацию на этих языках, поэтому он использует два класса в качестве обходного пути.

Вы можете сделать то же самое в Python, но вы также можете реализовать двойную диспетчеризацию с декоратором. (Lisp CLOS использует неопределенно похожий подход)

class A: pass
class B: pass

class visitor:
    def __init__(self, f):
        self.f = f
        self.cases = {}

    def case(self, type1, type2):
        def call(fun):
            self.cases[(type1, type2)] = fun
        return call


    def __call__(self, arg1, arg2):
        fun = self.cases[type(arg1), type(arg2)]
        return fun(arg1, arg2)

@visitor
def f(x, y): pass


@f.case(A, int)
def fun1(a, b):
    print("called with A and int")


@f.case(B, str)
def fun2(a, b):
    print("called with B and string")



f(A(), 5)
f(B(), "hello")