The Visitor pattern uses Multiple Dispatching. People can confuse the two by looking at the implementation rather than the intent.
The Visitor assumption is that you have a primary class hierarchy that is unchangeable. Perhaps it’s from another vendor and you can’t make changes to that hierarchy. However, you’d like to add new polymorphic methods to that hierarchy. Normally you’d need to add something to the base class interface, but that’s unchangeable. How do you get around this?
Visitor, the final pattern in GoF Design
Patterns, solves this kind of problem. It allows you to
extend the interface of the primary class hierarchy. It requires
that the primary class hierarchy have a method, typically called
accept(), which takes an object of a secondary
class hierarchy called Visitor. This virtualizes
the operations performed upon the primary hierarchy. The objects
of the primary hierarchy simply accept() the
Visitor, then call the Visitor’s
dynamically bound method:
# 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 self.__class__.__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 self.__class__.__name__
class Bug(Visitor):
pass
class Pollinator(Bug):
pass
class Predator(Bug):
pass
# Add the ability to do "Bee" activities:
class Bee(Pollinator):
def visit(self, flower: Flower) -> None:
flower.pollinate(self)
# Add the ability to do "Fly" activities:
class Fly(Pollinator):
def visit(self, flower: Flower) -> None:
flower.pollinate(self)
# Add the ability to do "Worm" activities:
class Worm(Predator):
def visit(self, flower: Flower) -> None:
flower.eat(self)
def flower_gen(n: int) -> Iterator[Flower]:
flwrs = Flower.__subclasses__()
for i in range(n):
yield random.choice(flwrs)()
# Now we can perform Bug operations on Flowers:
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 WormThe accept()/visit() pair is the
double dispatch. accept() resolves the
flower’s type, then visit() resolves the visitor’s
type, and the pollinate() or eat()
call inside visit() lands back on the flower’s
type. The last line of output shows both dispatches doing
visible work. Chrysanthemum overrides
eat() (chrysanthemums really do produce a natural
insecticide), so that line depends on both unknown types at
once: the worm’s type chose eat(), and the flower’s
type chose which eat() runs. Delete the
override and the program still runs; the flower-side dispatch
simply goes back to having nothing to say.
One annotation in the listing is load-bearing.
accept() types its visitor as Any,
because the Visitor base class declares no
visit() method, so visitor.visit(self)
would fail the type checker under an honest Visitor
annotation. The classic pattern fixes this by declaring
visit() abstract on the visitor base; a Python
version can give Visitor an abstract
visit(), or describe visitors with a
Protocol. The Any is the quiet price
of the empty base, the same bargain Data Transfer Objects
paid for its attribute bag.
Python can add a method to a fixed hierarchy from outside,
using functools.singledispatch. This turns a plain
function into one that dispatches on the type of its first
argument, with per-type implementations registered from
anywhere. This is how Visitor works, but without the
accept() hook or the Visitor class
hierarchy:
# 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))
#: Gladiolus: abundant nectar | fragrance: faint
#: Ranunculus: no nectar | fragrance: strong
#: Chrysanthemum: a little nectar | fragrance: faintEach registered implementation above is named _.
nectar() calls it through the dispatcher, never by
its own name, so the name carries no meaning. _ is
the conventional placeholder for a name nobody will use. Reusing
_ for every registration is safe:
@nectar.register stores the function in its
dispatch table before the next def _ rebinds the
name, so nothing is lost.
Nothing touches Flower. Each operation is a
separate function, and the @singledispatch default
handles any type you have not registered. Dispatch follows
inheritance: an unregistered subclass uses its nearest
registered ancestor, falling back to the Flower
default only when no ancestor is registered (the tests below pin
this down). A union annotation,
flower: Gladiolus | Ranunculus, registers one
implementation for several types at once. Adding a new operation
is a new function. Adding a new flower is a class and, where
needed, a one-line registration. When the operation should read
like a method, use functools.singledispatchmethod
instead.
Visitor still has a place: when you truly cannot
define functions over the hierarchy, or you need the
accept() hook for some other reason. But in Python
that is rare. As with Pattern
Refactoring’s price-and-weight example,
singledispatch is the open-method mechanism that
Visitor fakes.
Because each operation is a plain function, testing is
direct. Call it with each flower type and assert the result. The
cases worth covering are the registered types, the
@singledispatch default for an unregistered type,
and that the two operations dispatch independently:
# test_visitor.py
import pytest
from visitor_singledispatch import (
Chrysanthemum,
Flower,
Gladiolus,
Ranunculus,
fragrance,
nectar,
)
def test_nectar_registered_types() -> None:
assert nectar(Gladiolus()) == "Gladiolus: abundant nectar"
assert nectar(Chrysanthemum()) == "Chrysanthemum: a little nectar"
def test_nectar_default_for_unregistered() -> None:
assert nectar(Ranunculus()) == "Ranunculus: no nectar"
assert nectar(Flower()) == "Flower: no nectar"
@pytest.mark.parametrize("flower, expected", [
(Ranunculus(), "strong"),
(Gladiolus(), "faint"),
(Chrysanthemum(), "faint"),
])
def test_fragrance_registered_and_default(
flower: Flower, expected: str
) -> None:
assert fragrance(flower) == expected
def test_operations_dispatch_independently() -> None:
# Nectar knows Gladiolus and Chrysanthemum; fragrance knows
# Ranunculus. A Ranunculus falls to nectar's default but hits
# fragrance's registered case.
ranunculus = Ranunculus()
assert nectar(ranunculus) == "Ranunculus: no nectar"
assert fragrance(ranunculus) == "strong"
def test_dispatch_follows_inheritance() -> None:
# Unregistered subclass: nearest registered ancestor wins
class Hybrid(Gladiolus):
pass
assert nectar(Hybrid()) == "Hybrid: abundant nectar"Inhabitant: Dwarf (for engineers),
Elf (for marketers) and Troll (for
managers). Now create a class called Project that
creates the different inhabitants and causes them to
interact() with each other using Multiple
Dispatching.Inhabitant can randomly produce a
Weapon using get_weapon(): a
Dwarf uses Jargon or
Play, an Elf uses
InventFeature or SellImaginaryProduct,
and a Troll uses Edict and
Schedule. You must decide which weapons “win” and
“lose” in each interaction (as in
paper_scissors_rock.py). Add a
battle() method to Project that takes
two Inhabitants and matches them against each
other. Now create a meeting() method for
Project that creates groups of Dwarf,
Elf and Troll and battles the groups
against each other until only members of one group remain. These
are the “winners.”paper_scissors_rock.py with the table lookup of
paper_scissors_rock_table.py. When is the table
lookup more appropriate than hard-coding the dynamic dispatch?
Can you keep the syntactic simplicity of the dispatch while
using a table underneath?paper_scissors_rock_table.py.