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 declaring them in the base class interface. How do you add
them from outside the base class?
The Classic Visitor
Visitor, the final pattern
in GoF Design Patterns, requires one method on the
primary class hierarchy, typically called accept(),
which takes an object from a secondary hierarchy called
Visitor. accept() calls the visitor’s
visit(), passing self. The visitor’s
type chooses which visit() runs.
The accept() requirement limits where
Visitor applies. The hierarchy’s author must write
accept() in advance, because you cannot add it to a
vendor’s classes yourself.
Here, bugs visit flowers:
# flower_visitors.pyfrom 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:returntype(self).__name__class Gladiolus(Flower):passclass Ranunculus(Flower):passclass Chrysanthemum(Flower):@overridedef eat(self, eater: Visitor) ->None:print(self, "is toxic to", eater)# The secondary hierarchy accepted by Flower:class Visitor:def__str__(self) ->str:returntype(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):passclass Fly(Pollinator):passclass Worm(Predator):pass
A new Visitor subclass such as Fly
reuses an existing operation, so it leaves the primary hierarchy
as it is. In this Python form of the pattern, a new operation
whose behavior varies by flower type also needs a new method on
Flower. Two Departures
from the Classic Shape explains why.
Each flower accepts each bug in turn:
# bug_visits.pyfrom flower_visitors import (Bee, Chrysanthemum, Fly, Gladiolus, Ranunculus, Worm)bee = Bee()fly = Fly()worm = Worm()for flower in (Gladiolus(), Ranunculus(), Chrysanthemum()): flower.accept(bee) flower.accept(fly) flower.accept(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 Worm
In the classic pattern the
accept()/visit() pair is the double
dispatch. Every element class overrides accept(),
the override resolves the element’s type, and the
visit() call inside it resolves the visitor’s type.
In flower_visitors.py one
inherited 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 a step
later.
In the last line of bug_visits.py’s output,
both dispatches change the result, because
Chrysanthemum overrides eat()
(chrysanthemums really do produce a natural insecticide). 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(), so the visitor’s type alone decides
which method runs.
bug_visits.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.pyfrom flower_visitors import Chrysanthemum, Gladiolus, Wormworm = 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 Worm
The first dispatch resolves to Predator.visit
rather than Worm.visit, because Worm
inherits visit() from Predator.
Two Departures
from the Classic Shape
One annotation in flower_visitors.py looks
like a shortcut, but the empty base class requires it.
accept() types its visitor as Any
because the Visitor base class declares no
visit() method. If you declare that parameter as
Visitor, the type checker reports
visitor.visit as a missing 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)
flower_visitors.py keeps
the empty Visitor base and the Any in
accept(), to show what the Any gives
up: 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 and raise an AttributeError at runtime.
The Any in accept() is a choice,
since the Visits protocol above removes it. The
Any in Data
Transfer Objects is a necessity, because a bag of attributes
named at runtime makes Any the most precise type
you can write.
The classic pattern overloads
visit() once per flower type and keeps each
operation’s body in the visitor, so accept() is the
one method the pattern adds to the primary hierarchy. Python has
no method overloading, since a second def visit()
replaces the first. @overload
declares extra signatures for the type checker and leaves the
runtime with one implementation, which must branch on the
argument’s type. flower_visitors.py
therefore puts the type-specific behavior in
pollinate() and eat() on the flowers.
The visitors choose between those two methods. So flower_visitors.py’s
primary hierarchy holds the operations the pattern exists to
keep out of it.
To keep the operations in its own
methods, a Python visitor can build the method name from the
element’s class, as ast.NodeVisitor does. Its
visit() calls
getattr(self, "visit_" + node.__class__.__name__, self.generic_visit),
so a node class with no matching method falls back to
generic_visit(). The type checker cannot resolve a
method name built from a string at runtime.
The Pythonic
Visitor: singledispatch
functools.singledispatch adds
an operation to an existing hierarchy from outside, as
Visitor does, but without the accept()
method or the Visitor class hierarchy.
singledispatch turns a plain function into one that
dispatches on the type of its first argument, with per-type
implementations registered from anywhere. Here, the flowers 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.pyfrom functools import singledispatchclass Flower:def__str__(self) ->str:returntype(self).__name__class Gladiolus(Flower):passclass Ranunculus(Flower):passclass Chrysanthemum(Flower):pass# A new operation, defined outside the Flower hierarchy:@singledispatchdef nectar(flower: Flower) ->str:returnf"{flower}: no nectar"@nectar.registerdef _(flower: Gladiolus) ->str:returnf"{flower}: abundant nectar"@nectar.registerdef _(flower: Chrysanthemum) ->str:returnf"{flower}: a little nectar"# A second operation, added independently of the first:@singledispatchdef fragrance(flower: Flower) ->str:return"faint"@fragrance.registerdef _(flower: Ranunculus) ->str:return"strong"if__name__=="__main__": flowers: list[Flower] = [ Gladiolus(), Ranunculus(), Chrysanthemum()]for flower in flowers:print(nectar(flower), "| fragrance:", fragrance(flower))#: Gladiolus: abundant nectar | fragrance: faint#: Ranunculus: no nectar | fragrance: strong#: Chrysanthemum: a little nectar | fragrance: faint
Both operations live outside Flower, which stays
as it is.
@nectar.register reads the annotation on the
implementation’s first parameter. For
flower: Gladiolus, it 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.
A registered implementation’s name can 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.
Dispatch follows inheritance. A subclass resolves to its
nearest registered ancestor. @singledispatch
registers the base implementation under object,
from which every class descends. A type whose only registered
ancestor is object therefore gets the default.
nectar() exposes the dispatch table the
decorator built:
nectar.registry maps each registered type to its
implementation. nectar.dispatch(cls) reports the
implementation to which cls resolves.
Ranunculus is unregistered, so it resolves to the
same implementation Flower does, the one stored
under object.
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 quietly returns the default’s
no nectar string.
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 is 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 goes further when assert_never() sits in its
case _. 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 whose default answer is wrong
for it.
When the operation should be a
method of a class, use functools.singledispatchmethod.
singledispatchmethod dispatches on the first
argument after self.
Testing the Operations
Because each operation is a plain function, a test calls it
with each flower type and asserts the result. The tests cover
the registered types, the @singledispatch default
for an unregistered type, and a subclass that resolves to its
nearest registered ancestor:
Visitor still has a
place: when an object must loop over its elements inside
accept(), or when someone else’s framework calls
that method. Both cases are rare in Python. Pattern
Refactoring draws the same conclusion from its recycling_note.py
example.
A minimal example shows the first of those cases, a loop that
runs inside accept():
# recursive_accept.pyfrom typing import Any, Protocolfrom flower_visitors import Bee, Gladiolus, Ranunculus, Wormclass Accepts(Protocol):def accept(self, visitor: Any) ->None: ...# accept() drives the traversal itself:class Corsage:def__init__(self, *elements: Accepts) ->None:self.elements = elementsdef accept(self, visitor: Any) ->None:for element inself.elements: element.accept(visitor)if__name__=="__main__": corsage = Corsage( Gladiolus(), Corsage(Ranunculus()))for bug in (Bee(), Worm()): corsage.accept(bug)#: Gladiolus pollinated by Bee#: Ranunculus pollinated by Bee#: Gladiolus eaten by Worm#: Ranunculus eaten by Worm
The recursion works because Corsage and
Flower both define accept(). The loop
in Corsage.accept() makes the same call on each
element, element.accept(visitor), whether the
element is a flower or another corsage. The Accepts
protocol declares that shared method, so the type checker
reports an element that lacks accept(), such as
Corsage("rose"), before the program runs.
In bug_visits.py the loop
that calls accept() sits outside the hierarchy, at
module level, and takes one flower at a time from a tuple. In recursive_accept.py the
loop runs inside accept(), the situation where the
classic pattern still fits. Every visitor reuses that one loop.
A new visitor defines visit() for a single flower.
Corsage.accept() passes that visitor to every
nested element.
One Dispatch Is Enough
Visitor dispatches twice.
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
Flower.accept() calls visit() on the
worm, outside Flower, and
Predator.visit() calls eat() back on
the flower. The frames differ only in which eat()
that second call reaches. In the third frame no method on
Flower takes part. nectar()’s table,
keyed by the flower’s type, yields the implementation in one
lookup. That one dispatch resolves everything Visitor’s
two dispatches do.
The dispatch on the visitor’s type exists not because two
types are unknown, but because the operation must be a method on
some class. The visitor’s type chooses the operation, so the
language must resolve that type at runtime along with the
flower’s type. When 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. 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. The
Multiple Dispatching problem needs two. When both types
must resolve together at runtime, use the table from Multiple
Dispatching, keyed by a tuple of types.
Rewrite 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?
Add a 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 adds, and say which of the two changes
@singledispatch makes shorter.
Rewrite 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?
You are visitor 01402520 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.