Sal
Peter Hoffmann Director Data Engineering at Blue Yonder. Python Developer, Conference Speaker, Mountaineer

Extrinsic Visitor Pattern in Python with support for Inheritance

In python you often find a slightly modified version of the classic Visitor Pattern. Instead of using a accept(self, visitor)method in the target class to tell the visitor what kind of object to handle, the accept method is replaced with introspection in the visitor class:

An Extrinsic Visitor implements double dispatch with run time type information instead of Accept() methods. With the same machinery it is possible to test the feasibility of a particular visit before performing its operation. The extrinsic visitor pattern provides several benefits in ease of software development in trade for poorer run time performance. [Variations on the Visitor Pattern - Martin Nordberg]

But the implementation as shown in The Visitor Pattern Revisited has the drawback to not support inheritance:

There is one drawback to this approach, however. As commonly coded in Python, the Extrinsic Visitor pattern does not have any way to deal with inheritance. For example, let's say your PrettyPrinter class has a visit_list() method. And somebody subclasses list to make MyList. You're going to have to add a visit_MyList() to your PrettyPrinter class, even though in all probability the visit_list() method would work just fine. [The Visitor Pattern Revisited]
The following code shows a solution supporting inheritance using __mro__, the Python Method Resolution Order:
class Node(object): pass
class A(Node): pass
class B(Node): pass
class C(A,B): pass

class Visitor(object):
    def visit(self, node, *args, **kwargs):
        meth = None
        for cls in node.__class__.__mro__:
            meth_name = 'visit_'+cls.__name__
            meth = getattr(self, meth_name, None)
            if meth:
                break

        if not meth:
            meth = self.generic_visit
        return meth(node, *args, **kwargs)

    def generic_visit(self, node, *args, **kwargs):
        print 'generic_visit '+node.__class__.__name__

    def visit_B(self, node, *args, **kwargs):
        print 'visit_B '+node.__class__.__name__


a = A()
b = B()
c = C()
visitor = Visitor()
visitor.visit(a)
visitor.visit(b)
visitor.visit(c)

If you run the code you get the following output:

generic_visit A
visit_B B
visit_B C

For the class A the visitor falls back to the generic_visit method because there is no special implementation. For class B the visit_B method is used. And for class C the implementation walks up the __mro__ to find a suitable method from a superclass B visit_B.