The Visitor Pattern in Python
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