Card 06/ 09
Visitor Classes vs functools.singledispatch and match in Python
Python can choose a function by the argument's type without accept() on every class.
Python can choose code by an object's type without an accept method on every class, in two ways from the standard library and the language. This card writes an area total and an SVG export both ways, then sets out what each gives up beside the visitor classes.
functools.singledispatch turns a function into one that picks an implementation by the type of its first argument. match, from Python 3.10, compares a value against patterns in turn, and a class pattern such as Circle(radius=r) matches an instance of that class and pulls out its field.
import math
from dataclasses import dataclass
from functools import singledispatch
@dataclass
class Circle:
radius: float
@dataclass
class Square:
side: float
@singledispatch
def area(shape: object) -> float:
raise TypeError(f"no area for {type(shape).__name__}")
@area.register
def _(shape: Circle) -> float:
return math.pi * shape.radius**2
@area.register
def _(shape: Square) -> float:
return shape.side**2
def export(shape: object) -> str:
match shape:
case Circle(radius=r):
return f"<circle r='{r}'/>"
case Square(side=s):
return f"<rect width='{s}'/>"
case _:
raise TypeError(f"no export for {type(shape).__name__}")
shapes = [Circle(1), Square(2), Circle(0.5)]
print(f"total area: {sum(area(s) for s in shapes):.2f}")
print("\n".join(export(s) for s in shapes))total area: 7.93
<circle r='1'/>
<rect width='2'/>
<circle r='0.5'/>The output is a total area of 7.93 and three SVG tags. The shapes are plain dataclasses with no knowledge of any operation.
| Concern | Visitor classes | singledispatch | match |
|---|---|---|---|
| Code on the element classes | An accept method each | None | None |
| A missing element class | Caught when the visitor is created, if the methods are abstract | Caught at the call, by the fallback | Caught at the call, by case _ |
| State kept across visits | Fields on the visitor | Passed as arguments | Passed as arguments |
| Classes you cannot edit | Cannot add accept | Works | Works |
| Other languages | The same pattern in any object-oriented language | Python only | Python 3.10 and later, and similar features elsewhere |
Choose match for an operation small enough to read in one function, and singledispatch when each case is long, or when other modules must add cases for their own classes, which they can do with register. Choose visitor classes when an operation keeps state across a walk, such as counting calls over a whole program, or when the codebase is shared with a language that has neither feature. The fourth row matters most in practice: a library's classes cannot be given an accept method, and both alternatives work on them anyway.
← → move through sections and cards