A family of classes you cannot change needs a new operation. Visitor adds the operation from outside the family.
The Visitor pattern uses Multiple Dispatching. People can confuse the two by looking at the implementation rather than the intent.
Visitor assumes you have a primary class hierarchy you cannot change, perhaps because it comes from another vendor. You’d like to add new polymorphic methods to it, which normally means adding something to the base class interface. How do you add them from outside the base class?
Visitor, the final pattern in GoF Design
Patterns, solves the problem of a hierarchy you cannot
change. It lets you extend the interface of the primary class
hierarchy. It requires one method on the primary class
hierarchy, typically called accept(), which takes
an object from a secondary hierarchy called
Visitor. That one method is the catch. The
hierarchy’s author must write accept() in advance,
because you cannot add it to a vendor’s classes yourself.
accept() calls the visitor’s visit(),
passing self. The visitor’s type chooses which
visit() runs.
A new Visitor subclass that reuses an existing
operation, such as Fly, adds no code to the primary
hierarchy. In this Python version a new operation whose behavior
varies by flower type also needs a new method on
Flower, for a reason The Price of the Empty
Base explains below. In the listing, bugs visit flowers:
# flower_visitors.py
import random
from collections.abc import Iterator
from typing import Any, override
# The Flower hierarchy cannot be changed:
class Flower:
def accept(self, visitor: Any) -> None:
visitor.visit(self)
def pollinate(self, pollinator: Visitor) -> None:
print(self, "pollinated by", pollinator)
def eat(self, eater: Visitor) -> None:
print(self, "eaten by", eater)
def __str__(self) -> str:
return type(self).__name__
class Gladiolus(Flower):
pass
class Ranunculus(Flower):
pass
class Chrysanthemum(Flower):
@override
def eat(self, eater: Visitor) -> None:
print(self, "is toxic to", eater)
# The secondary hierarchy accepted by Flower:
class Visitor:
def __str__(self) -> str:
return type(self).__name__
class Bug(Visitor):
pass
# The middle layer names the operation:
class Pollinator(Bug):
def visit(self, flower: Flower) -> None:
flower.pollinate(self)
class Predator(Bug):
def visit(self, flower: Flower) -> None:
flower.eat(self)
# Concrete visitors, grouped by the operation they perform:
class Bee(Pollinator):
pass
class Fly(Pollinator):
pass
class Worm(Predator):
pass
def flower_gen(n: int) -> Iterator[Flower]:
flowers = Flower.__subclasses__()
for _ in range(n):
yield random.choice(flowers)()
# Now perform Bug operations on the flowers:
if __name__ == "__main__":
bee = Bee()
fly = Fly()
worm = Worm()
random.seed(47) # Reproducible flower sequence
for flower in flower_gen(4):
flower.accept(bee)
flower.accept(fly)
flower.accept(worm)
#: Ranunculus pollinated by Bee
#: Ranunculus pollinated by Fly
#: Ranunculus eaten by Worm
#: Gladiolus pollinated by Bee
#: Gladiolus pollinated by Fly
#: Gladiolus eaten by Worm
#: Ranunculus pollinated by Bee
#: Ranunculus pollinated by Fly
#: Ranunculus eaten by Worm
#: Chrysanthemum pollinated by Bee
#: Chrysanthemum pollinated by Fly
#: Chrysanthemum is toxic to Wormflower_gen() reads the concrete classes from
Flower.__subclasses__(), the same registry-free
enumeration as Factory.
The accept()/visit() pair is the
double dispatch. accept() passes the
concrete flower to the visitor, visit() resolves
the visitor’s type, and the pollinate() or
eat() call inside visit() resolves the
flower’s type. In the classic pattern every element class
overrides accept(), and the override resolves the
element’s type. In flower_visitors.py one
inherited accept() is enough, because the
pollinate() or eat() call resolves the
flower’s type a step later.
The last line of output is the one where both dispatches
change the result, because Chrysanthemum overrides
eat() (chrysanthemums really do produce a natural
insecticide). That line depends on both unknown types at once.
The worm’s type chooses eat(), and the flower’s
type chooses which eat() runs. If you delete the
override, every flower resolves to the same
Flower.eat(), and the visitor’s type alone decides
which method runs.
flower_visitors.py’s
output shows results, not mechanism. Printing the qualified name
of the method to which each dispatch resolves makes both
dispatches visible:
# dispatch_trace.py
from flower_visitors import Chrysanthemum, Gladiolus, Worm
worm = Worm()
for flower in (Chrysanthemum(), Gladiolus()):
print(type(worm).visit.__qualname__,
"then", type(flower).eat.__qualname__)
flower.accept(worm)
#: Predator.visit then Chrysanthemum.eat
#: Chrysanthemum is toxic to Worm
#: Predator.visit then Flower.eat
#: Gladiolus eaten by WormThe first dispatch resolves to Predator.visit
rather than Worm.visit, because Worm
inherits the operation from the class that defines it. For
Chrysanthemum the second dispatch resolves to the
override, and for Gladiolus to the inherited
Flower.eat. The trace shows that flower-side
choice, which flower_visitors.py’s
output leaves implicit.
One annotation in flower_visitors.py looks
like a shortcut and is not. accept() types its
visitor as Any because the Visitor
base class declares no visit() method. If you
declare that parameter as Visitor instead, the type
checker reports visitor.visit as an unresolved
attribute. The classic pattern declares visit()
abstract on the visitor base, so visitor.visit
resolves. A Protocol that declares
visit() removes the Any:
class Visits(Protocol):
def visit(self, flower: Flower) -> None: ...
class Flower:
def accept(self, visitor: Visits) -> None:
visitor.visit(self)
The chapter still keeps the empty Visitor base
and the Any in accept(), because
showing what the Any gives up is part of the point.
What it gives up is every check on the visitor side.
Gladiolus().accept(Bug()) passes the type checker
and fails at runtime with
AttributeError: 'Bug' object has no attribute 'visit'.
In Multiple
Dispatching, the Any in paper_scissors_rock.py
lets a class that omits an eval_*() method pass the
checker the same way. The Any in
accept() is a choice, unlike the one in Data
Transfer Objects, where a bag of attributes named at runtime
leaves no precise type to write.
Notice which hierarchy holds the behavior. The classic
pattern overloads visit() once per flower type and
keeps each operation’s body in the visitor, so
accept() is the one method it adds to the primary
hierarchy. Python has no method overloading, since a second
def visit() replaces the first. @overload
does not change that. It declares extra signatures for the type
checker, and the one implementation must still branch on the
argument’s type. flower_visitors.py
therefore puts the type-specific behavior in
pollinate() and eat() on the flowers
instead, and the visitors choose between them. So flower_visitors.py’s
primary hierarchy holds the operations the pattern exists to
keep out of it. A Python visitor can keep them instead by
building the method name from the element’s class, as
ast.NodeVisitor does with
getattr(self, "visit_" + node.__class__.__name__),
but the type checker cannot see which method a string names.
Python can add an operation to an existing hierarchy from
outside, using functools.singledispatch.
singledispatch turns a plain function into one that
dispatches on the type of its first argument, with per-type
implementations registered from anywhere. Adding an operation
from outside is what Visitor does, and
singledispatch adds one without the
accept() method or the Visitor class
hierarchy. The flowers below are the same three as in flower_visitors.py. The
two operations, nectar() and
fragrance(), are new, each added independently of
the other:
# visitor_singledispatch.py
from functools import singledispatch
class Flower:
def __str__(self) -> str:
return type(self).__name__
class Gladiolus(Flower):
pass
class Ranunculus(Flower):
pass
class Chrysanthemum(Flower):
pass
# A new operation, defined outside the Flower hierarchy:
@singledispatch
def nectar(flower: Flower) -> str:
return f"{flower}: no nectar"
@nectar.register
def _(flower: Gladiolus) -> str:
return f"{flower}: abundant nectar"
@nectar.register
def _(flower: Chrysanthemum) -> str:
return f"{flower}: a little nectar"
# A second operation, added independently of the first:
@singledispatch
def fragrance(flower: Flower) -> str:
return "faint"
@fragrance.register
def _(flower: Ranunculus) -> str:
return "strong"
if __name__ == "__main__":
flowers: list[Flower] = [
Gladiolus(), Ranunculus(), Chrysanthemum()]
for f in flowers:
print(nectar(f), "| fragrance:", fragrance(f))
print(sorted(t.__name__ for t in nectar.registry))
print(nectar.dispatch(Ranunculus)
is nectar.dispatch(Flower))
#: Gladiolus: abundant nectar | fragrance: faint
#: Ranunculus: no nectar | fragrance: strong
#: Chrysanthemum: a little nectar | fragrance: faint
#: ['Chrysanthemum', 'Gladiolus', 'object']
#: True@nectar.register reads the annotation on the
implementation’s first parameter. flower: Gladiolus
stores that implementation in the dispatch table under the key
Gladiolus. A union annotation,
flower: Gladiolus | Ranunculus, registers one
implementation for several types at once.
Each registered implementation takes the name _.
The name is free to be anything, since a call to
nectar() finds that implementation through the
dispatch table. _ is the conventional placeholder
for a name that exists only to satisfy def. Reusing
_ for every registration is safe.
@nectar.register stores the function in its
dispatch table before the next def _ rebinds the
name, so every implementation stays in the table.
Nothing edits Flower. Each operation is a
separate function.
Dispatch follows inheritance. A subclass resolves to its
nearest registered ancestor. Every class descends from
object, and @singledispatch registers
the base implementation under object, so a type
with no other registered ancestor gets the default.
visitor_singledispatch.py’s
last two output lines inspect the dispatch table the decorator
built. nectar.registry maps each registered type to
its implementation, and nectar.dispatch(cls)
reports the implementation to which cls resolves.
Nothing registers Ranunculus, so
Ranunculus resolves to the same implementation
Flower does, the one stored under
object.
The default is also the risk. A new Flower
subclass gets the default answer until someone registers it.
Both the runtime and the checker accept the
nectar() call on that subclass, so a forgotten
registration produces a wrong result rather than a failure.
The default also accepts arguments outside
Flower. Because the default sits under
object, not under the Flower in its
annotation, nectar(42) returns
42: no nectar. ty and Pyright accept
nectar(42) too, because the dispatcher that
@singledispatch builds declares its parameters as
Any. Only mypy rejects it, since mypy’s built-in
singledispatch plugin checks the call against the
base function’s signature.
When the default answer would be wrong for an unregistered
type, give the base function a
raise NotImplementedError(f"no nectar rule for {type(flower).__name__}")
in place of the fallback string. A forgotten registration then
fails at its first call. A match over a union of
types, with assert_never() in its
case _, goes further. The type checker reports
the omission before anything runs. The set of types is then
closed, since adding a type means editing the union.
Under @singledispatch, adding a new operation is
a new function. Adding a new flower is a class, plus one
registration for each operation that needs more than the
default.
When the operation should read like a method, use functools.singledispatchmethod
instead. singledispatchmethod dispatches on the
first argument after self.
Because each operation is a plain function, a test calls it
with each flower type and asserts the result. The cases worth
covering are the registered types, the
@singledispatch default for an unregistered type,
and a subclass that resolves to its nearest registered
ancestor:
# test_visitor.py
import pytest
from visitor_singledispatch import (Chrysanthemum, Flower,
Gladiolus, Ranunculus,
fragrance, nectar)
@pytest.mark.parametrize("flower, expected", [
(Gladiolus(), "Gladiolus: abundant nectar"),
(Chrysanthemum(), "Chrysanthemum: a little nectar"),
(Ranunculus(), "Ranunculus: no nectar"),
(Flower(), "Flower: no nectar"),
])
def test_nectar_registered_and_default(
flower: Flower, expected: str
) -> None:
assert nectar(flower) == expected
@pytest.mark.parametrize("flower, expected", [
(Ranunculus(), "strong"),
(Gladiolus(), "faint"),
(Chrysanthemum(), "faint"),
(Flower(), "faint"),
])
def test_fragrance_registered_and_default(
flower: Flower, expected: str
) -> None:
assert fragrance(flower) == expected
def test_dispatch_follows_inheritance() -> None:
class Hybrid(Gladiolus):
pass
assert Hybrid not in nectar.registry
assert nectar(Hybrid()) == "Hybrid: abundant nectar"Visitor still has a place: when an object must loop
over its own elements inside accept(), or when
someone else’s framework calls that method. Both cases are rare
in Python. The recycling_note.py example
in Pattern
Refactoring reaches the same conclusion.
A minimal example shows the first of those cases, a loop that
runs inside accept(). Corsage.accept()
calls accept() on each of its elements and recurses
into any nested Corsage, so no code outside the
object loops over the elements:
# recursive_accept.py
from typing import Any
from flower_visitors import Bee, Gladiolus, Ranunculus
# accept() drives the traversal itself:
class Corsage:
def __init__(self, *elements: Any) -> None:
self.elements = elements
def accept(self, visitor: Any) -> None:
for element in self.elements:
element.accept(visitor)
if __name__ == "__main__":
corsage = Corsage(
Gladiolus(), Corsage(Ranunculus()))
corsage.accept(Bee())
#: Gladiolus pollinated by Bee
#: Ranunculus pollinated by BeeThe recursion works because Corsage and
Flower both define accept(). The loop
in Corsage.accept() calls accept() on
each element the same way, whether the element is a flower or
another corsage. In flower_visitors.py the
loop that calls accept() sits outside the
hierarchy, in the main block, and takes one flower at a time
from flower_gen(). In recursive_accept.py the
loop runs inside accept(), the situation where the
classic pattern still fits.
Visitor dispatches twice, and
singledispatch dispatches once. The figure sets
Visitor and singledispatch side by side,
with the worm from dispatch_trace.py and the
nectar() operation from visitor_singledispatch.py:
In the first two frames the call crosses the boundary of
Flower twice, out to the visitor and back again,
and the frames differ only in the method the return trip
reaches. In the third frame no method on Flower
takes part. One lookup in nectar()’s table, keyed
by the flower’s type, finds the answer. That one dispatch
resolves everything Visitor’s two dispatches do.
The second dispatch in the classic pattern exists not because
two types are unknown, but because the operation must be a
method on some class. The visitor’s type stands in for the
operation, so the language must resolve that type at runtime
along with the element’s type. Once an operation can be a
function defined outside the hierarchy, the call site names the
operation, so writing nectar() rather than
fragrance() selects it before the program runs. The
flower’s type is the one thing left for runtime to resolve, and
one dispatch covers it.
Visitor adds operations to a hierarchy you cannot
edit, and its double dispatch is the means. Multiple
Dispatching is the end in itself. Two objects must
interact, and both their types stay unknown until runtime, as in
paper_scissors_rock.py.
singledispatch resolves one argument’s type, and
the Multiple Dispatching problem needs two. When two
types must genuinely resolve together, use the table keyed by a
tuple of types from Multiple
Dispatching.
This chapter’s solutions give a hint, usually the shape of the code, and a full answer for each exercise.
flower_visitors.py with
singledispatch. Make pollinate() and
eat() functions defined outside the
Flower hierarchy, with Chrysanthemum’s
toxicity a registered implementation of eat().
Which classes and which methods disappear?Rose to visitor_singledispatch.py
with abundant nectar and a strong fragrance, then add a third
operation, thorns(), over all four flowers. Count
the lines each change costs, and say which of the two changes
@singledispatch makes cheaper.flower_visitors.py with
the Visits protocol in place of Any,
so accept() declares what it needs. Then add a
Beetle(Bug) with no visit() method and
pass it to accept(). Which version reports the
mistake, and when?