Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Visitor Pattern in Python

Tavian Barnes Comments

The visitor pattern is tremendously useful when working with certain kinds of information like abstract syntax trees. It’s basically a poor man’s version of sum types for languages that don’t natively support them. Unfortunately, they take advantage of function overloading, something which duck-typed languages like Python lack.

This blog post by Chris Lamb presents a clever workaround, but stops short of giving the actual implementation for the relevant decorators. The idea looks like this:

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!"

It looks a little suspicious (after all, we’ve defined three conflicting methods on the same class), but you can write @visitor in a way that makes it work:

# 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

The trick here is that the decorator replaces all the visit methods with _visitor_impl (redefining an existing method is fine in Python). But before it does that, it stores the original method in a dictionary, _methods, keyed by the visitor class and the desired argument type. Then, when visit is invoked, _visitor_impl looks up the appropriate implementation and invokes it based on the argument type.


Comments

Kenji Noguchi

Here is another implementation.
https://github.com/realistschuckle/pyvisitor