Adding a type should mean writing one
class, but something in the program must still choose which class to
construct. A Factory puts that choice in one place.
When a system needs new types, start with a base type that
gives them a common interface. Code that calls through that
interface never names the specific types you add. Adding a type
then means writing one subclass, with no changes to existing
code … or so it seems. But something must still create an object
of the new type, and that creation code names the concrete
class. If your application creates objects in many places,
adding a type means finding and editing every place that
specifies a concrete class.
Here Triangle has just joined the hierarchy. Two
call sites build shapes by calling Circle() or
Square(), and neither handles Triangle
yet:
The two call sites fail in different ways.
render() accepts "Triangle", falls
through its if chain, and returns silently.
export_svg() has a wildcard case that raises an
exception, so it reports the gap at runtime, the first time
someone asks it for a triangle.
At edit time, the type checker accepts both functions.
kind: str gives the type checker no list of the
strings export_svg() should handle. If
kind is an Enum and the wildcard calls
assert_never(), that report moves
to check time. An if-chain like render() has no
wildcard, so it still gets no report. Either way, adding a type
means editing every call site.
The solution is to encapsulate
object creation. A common factory creates every object
instead of spreading creational code through the system. Your
program must call this factory whenever it needs an object, so
adding a new type changes the factory and leaves every call site
as it was.
Every object-oriented program creates objects. To extend such
a program, you often add new types. Thus, Factory might
be the most common design pattern.
This chapter covers the creational
patterns of GoF Design Patterns: Factory
Method, Abstract Factory, Prototype, and
Builder. The fifth, Singleton, has its own chapter. All five
answer two questions: which object to build, and what code
builds it.
Simple Factory Method
Consider a Shape
hierarchy in the style of Rethinking
Objects, here with one method, draw(). We can
add a factory as a @staticmethod of the base
class:
# shape_factory_method.pyimport randomfrom abc import ABC, abstractmethodfrom collections.abc import Iteratorfrom typing import overrideclass Shape(ABC):@abstractmethoddef draw(self) ->None: ...# Create based on class name:@staticmethoddef factory(kind: str) -> Shape:match kind:case"Circle":return _Circle()case"Square":return _Square()case _:raiseValueError(f"Bad shape: {kind}")class _Circle(Shape):@overridedef draw(self) ->None: print("Circle.draw")class _Square(Shape):@overridedef draw(self) ->None: print("Square.draw")def shape_name(n: int) -> Iterator[str]:for _ inrange(n): cls = random.choice(Shape.__subclasses__())yield cls.__name__.removeprefix("_")if__name__=="__main__": random.seed(4) # Reproducible shape sequence shapes = [Shape.factory(s) for s in shape_name(4)]for shape in shapes: shape.draw()#: Circle.draw#: Square.draw#: Circle.draw#: Square.draw
Here the factory() argument is a string, but it
can be any sort of data. Apart from the new subclass,
factory() is the only code that changes when you
add a new type of Shape.
factory() names _Circle and
_Square above the point where the file defines
either class. Python looks up a name in a function body when the
function runs. Both classes exist before anything calls
factory(), so each lookup finds its class.
GoF Design Patterns defines Factory Method
as a creation method that subclasses override to
choose the concrete type. This factory() is the
smallest version of that idea: one class, one method, and a
match where the overrides go.
shape_name() is a generator.
A factory takes information that tells it what to build. A
generator object produces its next value from its own state,
because it holds the algorithm that decides what comes next.
Normally the data passed to factory() comes from
outside the system rather than from a random number
generator.
Inside shape_name(),
Shape.__subclasses__() produces a list of
Shape’s direct subclasses. Because
__subclasses__() stops at the first level of
inheritance, a class inheriting from _Circle is not
in the list. For a deeper hierarchy, recurse through each
subclass’s own __subclasses__() (see exercise
9).
Hiding the Concrete
Classes
The concrete shapes carry a leading underscore because
callers get them through factory().
factory() returns Shape, so a caller
works with Shapes, and _Circle stays a
name inside the module. shape_name() strips the
underscore, so the name a caller passes to
factory() is "Circle", not
_Circle.
The underscore discourages direct
construction, but it is a convention rather than concealment. Singleton
says the same of _Settings. Python honors its
underscore in two places: from module import *
leaves out the class, and help() leaves it out of
the module’s listing.
Nesting the classes inside
factory() looks like stronger enforcement, but is
worse. Because a class statement is executable
code, every factory() call defines fresh
Circle and Square classes. Two shapes
from different calls then share behavior but belong to different
classes, failing type(a) is type(b) and
isinstance() alike.
Shape.__subclasses__() is empty until the first
call, then gains a duplicate Circle and
Square on every call after that. Each duplicate
stays in the list until the garbage collector reclaims it, after
the program drops its last instance.
Alternative
Constructors Are Factories
from_fahrenheit()
is an alternative constructor, a method on the type that takes
data in another form and converts it into the constructor’s
arguments. It is also a factory of the same kind as
factory(): each takes data and returns an instance.
The usual alternative constructor is a @classmethod
like from_fahrenheit(), which computes the
constructor’s arguments and ends with
return cls(...). That form is the most common
factory. The standard library’s dict.fromkeys() and
datetime.fromisoformat() are two examples.
The @classmethod form chooses arguments rather
than a class. cls is the class that receives the
call, so a subclass that calls the inherited method gets an
instance of the subclass.
An Enum makes its constructor a factory over a
closed set. In Enums
Are Types Too, each Month member’s value is its
month number, so Month(7) returns the existing
member JULY rather than building a new object.
Month(13) raises a ValueError, as
factory() raises one for a name it does not
recognize. Where factory() has a match
that lists each class, the Enum looks up the value
in a table of its members, because the class holds every member
the call can return. A factory over a closed set of products
reduces to a lookup.
The Pythonic Factory:
a Dictionary
A factory turns data, such as a
name, into an object, so constructor calls stay in one place. A
class is a first-class object. You can store it in a variable
and call it to construct an instance. defaultdict(list)
and field(default_factory=list)
both take a class as the zero-argument callable they call
whenever they need a fresh value.
Thus, the simplest factory is a dictionary that maps names to
classes:
The dict values have
the type type[Shape] because they are classes, and
calling one constructs an instance. If the table holds
instances, as in {"Circle": Circle()}, and
make() returns SHAPES[kind] without
calling it, every caller shares one Circle. Prototype keeps a table of
instances on purpose and copies one for each caller.
Adding a Triangle
means one new class, one new line in SHAPES, and
one new member in Kind. The checker rejects a
SHAPES key outside Kind, so a new
shape name goes into the Literal before it can go
into SHAPES. Typing kind as the closed
Literal instead of str moves a bad
name from a runtime KeyError to a check-time error,
as Explicit
Registration with a Protocol does for a class that lacks
draw().
Self Registration
Better still, a new
Shape subclass can register itself with no edit to
existing code. Self registration rules out a closed
Literal, which needs an edit for every new
subclass, so make() here takes a
str.
With __init_subclass__(),
each subclass registers itself. The two class
statements fill Shape.registry on their own.
Shape is an abstract base class rather than a
Protocol because it registers subclasses through
__init_subclass__().
__init_subclass__() runs for each class that
inherits from Shape, so a class that satisfies a
Protocol by structure alone stays out of the registry.
Inheritance is the mechanism.
ABC adds one guard on
top of inheritance. A subclass registers as its
class statement executes, so a subclass that lacks
draw() still registers. The guard acts when
make() constructs that class. The call fails with a
TypeError. A base class whose draw()
raises a NotImplementedError reports the omission
later, at the first draw() call.
The type checker reports a line that constructs such a class
by name, but make() constructs the class it looks
up in the registry. Shape.registry[name]() calls a
type[Shape]. Any type[Shape] may be a
concrete subclass, so the checker accepts the call. In Explicit
Registration with a Protocol, the checker reports a class
that lacks draw() at the line that registers
it.
Adding a Triangle
takes a single class definition, and make() builds
it with no change to the factory. Pattern
Refactoring uses this self-registration.
Shape.__subclasses__() can
build the registry, but it stops at direct subclasses.
__init_subclass__() runs for every class anywhere
below Shape, so a class that inherits from
Circle registers as well.
Importing registry runs its two
class statements. The key list shows the names they
registered:
The closed Literal in shape_table.py rejects
"Hexagon" before the program runs. An open registry
cannot do that. A name becomes valid the moment some module
defines its class, so the check moves to runtime.
The figure sets the opening problem from shapes_naive.py beside the
registry, so you can compare the edits that adding
Triangle requires before and after the factory:
In the first frame, each call site holds its own arrow to
every class, so a new class needs a new arrow, and an edit, at
each call site. In the second frame the arrows run the other
way: each class points at the registry. From then on a caller
points at make() alone, and make()
points at the class it finds. Triangle adds one
arrow, from its class statement.
Hazards of Self
Registration
__init_subclass__()
runs as the subclass’s class statement executes.
When the subclasses sit in the same file as make(),
as in registry.py, the
registration runs before anything calls make(). A
subclass defined in another module registers itself only when
something imports that module. The classic failure is a plugin
that “never registered.” The class is fine, the registry is
fine, and the module that defines the class still waits for its
first import.
A lazy
import produces the same failure even when the import
statement is in the file. The module body, and with it the
registration, runs at the first use of the imported name. An
import written only to trigger registration never uses that
name. Running with -X lazy_imports=all makes
ordinary imports lazy along with the marked ones, so the same
failure can appear in a program with no lazy
keyword in it. Import a plugin module eagerly when the import
exists for its side effect.
The registry stores each class
under cls.__name__ alone, so when two classes from
different modules share a name, the second to register silently
replaces the first. Key on
f"{cls.__module__}.{cls.__qualname__}" when a
collision is possible.
The registry also keeps every
entry it receives. A class defined inside a function or a test
stays in the registry. The registry’s strong reference keeps the
class alive for the rest of the process.
__init_subclass__() names
Shape.registry rather than
cls.registry on purpose. cls.registry
resolves through the MRO,
so a subclass that defines its own registry creates
a second registry beside the one make() reads, with
no error to signal it.
make() stays a
module-level function for three reasons. A
@classmethod looks up cls.registry.
When a subclass such as Triangle defines its own
registry, Triangle.make() reads that
second registry, the one __init_subclass__() avoids
by naming Shape.registry. And
Circle.make("Square") is legal as well as
misleading, since the key decides what make()
builds, not the class you name before the dot. A method of any
kind also puts back the factory method that The Pythonic Factory:
a Dictionary set out to remove.
Testing the Registry
Defining a fresh subclass of Shape inside
test_new_subclass_registers_itself() is enough to
put it in the registry:
test_unknown_name_raises_key_error() asks for
"Hexagon" rather than the "Triangle"
that registry_demo.py used,
because the Triangle that
test_new_subclass_registers_itself() defined is
still in the registry.
Explicit
Registration with a Protocol
registry.py needs a base
class because Python calls the base class’s
__init_subclass__() for each new subclass. If
registration is explicit, Shape can be a Protocol,
with a class decorator doing the registering. The table then
moves out of the base class and into the factory that reads it.
Python lets you set
an attribute on a function, but the type checker reports
every dotted access to such an attribute. Instead, the factory
becomes a small callable object that holds the table:
register() stores a class under its name and
returns the class unchanged, so it works as a class
decorator. __call__()
makes a ShapeFactory instance callable, so you call
the factory the way you call make() in registry.py. Each class
registers with the factory that builds it:
# protocol_registry.pyfrom shape_registry import ShapeFactorymake = ShapeFactory()@make.registerclass Circle:def draw(self) ->None: print("Circle.draw")@make.registerclass Square:def draw(self) ->None: print("Square.draw")print(sorted(make.registry))#: ['Circle', 'Square']make("Circle").draw()#: Circle.draw# ty: Argument type `Blob` does not satisfy# upper bound `Shape` of type variable `S`:# @make.register# class Blob:# pass
@make.register has the same form as
@nectar.register in Visitor,
where functools.singledispatch stores a function’s
implementations, keyed by type, on the function itself.
singledispatch cannot serve as this factory,
because it picks an implementation by the type of its first
argument. make() receives a name.
The type parameter of register() has
Shape as its bound. The bound turns the decorator
into a check: a decorated class must satisfy the Protocol. If a
class lacks draw(), or its draw()
takes an extra parameter, the checker reports
invalid-argument-type at the class’s
@make.register line before the program runs. Self Registration leaves the same
omission to runtime, where a subclass that lacks
draw() registers, fails at construction, and gets
no report from the checker.
The bound is also why the factory names Shape
instead of taking a type parameter. A generic factory needs
register()’s bound to name the factory’s own type
parameter. The type checker rejects a bound that contains
another type parameter.
register() returns type[S], the
decorated class’s own type, so the checker sees the decorated
name Circle as type[Circle], not
type[Shape].
Keeping the table in the factory removes two hazards from Hazards of Self
Registration. The table belongs to make rather
than to a class in the hierarchy, so a lookup reads the
factory’s one table rather than walking a class’s MRO.
make is a ShapeFactory outside the
shape classes, so a call like Circle.make("Square")
raises an AttributeError instead of building a
Square.
An intermediate class also stays out of the table unless
something decorates it. Under __init_subclass__(),
an abstract Polygon between Shape and
Triangle registers as well, so
make("Polygon") fails with a
TypeError.
Explicit registration fails the opposite way. Registration is
opt-in, so a class that satisfies Shape but lacks
@make.register is absent from the table.
make() then fails with a KeyError that
names the key, not the class that lacks the decorator (see
exercise 10). A subclass of the ABC always registers, because
the subclass line is the registration.
The runtime guard is weaker too. If you run the program
despite the checker’s report, a class that lacks
draw() still registers, and its first
draw() call fails with an
AttributeError rather than its construction failing
with a TypeError.
Choose the failure you prefer. The ABC registers every
subclass and catches an incomplete one at construction. The
Protocol catches an incomplete class at check time and leaves
registration to you.
A factory that is an object also gives each test its own
table:
# test_protocol_registry.pyimport pytestfrom shape_registry import ShapeFactoryclass Triangle:def draw(self) ->None: ...def test_register_stores_and_returns_the_class() ->None: make = ShapeFactory()assert make.register(Triangle) is Triangleassert make.registry == {"Triangle": Triangle}def test_make_builds_a_registered_class() ->None: make = ShapeFactory() make.register(Triangle)assertisinstance(make("Triangle"), Triangle)def test_each_factory_starts_empty() ->None: make = ShapeFactory()with pytest.raises(KeyError): make("Triangle")
test_each_factory_starts_empty() passes although
test_make_builds_a_registered_class() registered
Triangle, because each test registers with a
ShapeFactory of its own. Every test in test_registry.py shares
Shape.registry, one registry for the whole process,
so test_unknown_name_raises_key_error() there asks
for "Hexagon".
The ordinary Python factory is a dictionary of classes,
whether you write it as a literal, the classes fill it
themselves, or the factory’s own decorator fills it. That is the
dissolution Design
Patterns describes. The pattern remains, but no longer needs
a class hierarchy to express it.
Factory Objects
Because the static factory() method in shape_factory_method.py
collects all the creation operations in one place, that method
is the only existing code you change when you add a shape.
factory() chooses what to build with a
match statement on its string argument. A
factory object defines a single create()
method, so choosing what to build becomes choosing which factory
object to call. Here, we create one factory object per
Shape subtype:
ShapeMaker is the interface the nested
Factory classes share. Here it is a
Protocol, so the two classes define a matching
create() to conform to it.
Python does not need a Factory class nested in
every shape. shape_factory_objects.py
includes one because a language that cannot store a class in a
dictionary must wrap each constructor in an object. registry.py stores each
class itself in the dictionary, with no nested classes.
A separate factory object is worth writing when it holds
state, such as a pool, a cache, or loaded configuration, or when
the caller keeps the factory to construct objects later.
To eliminate
FACTORIES, you can dispatch through
eval(f"_{kind}.Factory()"). Dispatching through
eval() is unnecessary, and it makes
create_shape() worse. With eval(),
create_shape() compiles and runs any string it
receives, so a configuration file, a request, or a command line
can hand it arbitrary code instead of a shape name. The
dictionary lookup treats the string as a key. You get either a
factory or a KeyError. The factory has the type
ShapeMaker that FACTORIES declares,
where eval() returns Any.
Subclasses Choose the
Type
Every factory so far keeps the
choice in one place: a match in
factory(), a key in SHAPES, a lookup
in FACTORIES. Factory Method moves the
choice into the type of the object you hold. A base class calls
an abstract creation method, and each subclass overrides that
method to specify a concrete product:
new_shape() is the factory method, and the only
method CircleSketch and SquareSketch
override. Adding a Triangle means one new
Shape subclass and one new Sketch
subclass, with no edit to existing code. Overriding a creation
method in a subclass is the form GoF Design Patterns
describes, and the reason the pattern takes its name from a
method rather than a class.
Factory Method adds a second hierarchy. Each product
needs a factory that produces it, so the two hierarchies grow
together. The second hierarchy is justified only when the
factory does work of its own, as render() does
here. When choosing the class is the factory’s only job, a
dictionary of classes maps each name to its class in one table:
shape_table.py when the
set of shapes is closed, protocol_registry.py when
it is open.
Abstract Factories
The Abstract Factory
pattern has the same structure as Sketch, with
several factory methods where Sketch has one, each
overridden in a concrete factory. Each factory method creates a
different kind of object. Creating the factory object chooses
the concrete version of every object that factory creates.
The example in GoF Design Patterns makes one user
interface toolkit work across several look-and-feel standards,
such as Motif and Presentation Manager. You create a factory
object for the look and feel you use. From then on, when you ask
that factory for a scroll bar, window, or button, it creates the
version of that widget suited to that look and feel. Switching
from one look and feel to another then changes one place in the
code, most likely via startup configuration.
As another example, suppose you are creating a
general-purpose gaming environment that supports different types
of games.
Character and
Obstacle are parallel hierarchies, and each
concrete factory declares one method per hierarchy.
KittiesAndPuzzles always pairs a Kitty
with a Puzzle, and WarriorsAndWeapons
always pairs a Warrior with a Weapon.
GameEnvironment.play() depends on getting such a
matched pair. Choosing the factory chooses both halves at
once:
The GameEnvironment controls the setup and play
of the game. Setup and play are simple here, but the initial
conditions and the way the state changes can determine much of a
game’s outcome. GameEnvironment writes the rules of
play into play(), so a real game adds a place to
vary them: a subclass overriding play(), or a rules
object passed alongside the factory.
interact_with() dispatches on the character’s
type, and obstacle.description() dispatches again
on the obstacle’s type. Thus, the line play()
prints depends on both types. Multiple
Dispatching develops that pair of calls into a
technique.
Suppose you write a factory subclass and forget
make_obstacle(). Python defines the class and
raises a TypeError when you create an instance,
before GameEnvironment.__init__() calls anything.
The type checker reports that construction before the program
runs. Python raises that TypeError for any class
that leaves an abstract method unimplemented. At runtime,
make() in registry.py raises it on a
Shape subclass that forgets draw(),
and so does Partial() in Surrogate.
A Protocol simplifies the Abstract Factory. The
Protocol specifies the required methods. To satisfy it, a
concrete class defines them. Here, the concrete classes have no
base class:
The type checker verifies that each concrete class satisfies
the appropriate Protocol. If you uncomment
[1], which passes a BrokenFactory to
GameEnvironment, the checker reports
protocol member make_obstacle is not defined on type BrokenFactory.
Both versions report the omission before the program runs, at
different places. With the abstract base classes in abstract_factory_abc.py,
the checker reports the line that constructs the incomplete
factory. With the Protocol, constructing a
BrokenFactory is legal. The checker reports
[1], the line that passes the factory to
GameEnvironment.
The two versions differ more at runtime. Constructing the
incomplete subclass of the abstract base class raises a
TypeError, while a BrokenFactory
constructs at runtime like any other class. If you run the
program despite the report, it fails with an
AttributeError when
GameEnvironment.__init__() calls
make_obstacle().
Checking against a Protocol is structural
typing. With structural typing, the checker still reports an
incomplete class, and no concrete class inherits from an
interface.
Prototype
The factories so far build each object from a class and some
arguments. Prototype keeps one fully configured
instance and copies it to make new objects. Use
Prototype when a ready-made instance is easier to clone
than to construct, or when construction is slow and the
instances share most of the setup.
The copy module does
the cloning. copy.deepcopy() follows every
reference, so the clone gets its own copy of every mutable
object the original holds:
# prototype.pyimport copyfrom dataclasses import dataclass, field@dataclassclass Monster: name: str hp: int powers: list[str] = field(default_factory=list)def clone(self) -> Monster:return copy.deepcopy(self)goblin = Monster("Goblin", hp=10, powers=["bite"])# Build a variant by cloning and adjusting:captain = goblin.clone()captain.name ="Captain"captain.hp =20captain.powers.append("rally")print(goblin)#: Monster(name='Goblin', hp=10, powers=['bite'])print(captain)#: Monster(name='Captain', hp=20, powers=['bite', 'rally'])# Rebuild through the constructor with new field values:knight = copy.replace(goblin, name="Knight", hp=30)print(knight)#: Monster(name='Knight', hp=30, powers=['bite'])print(knight.powers is goblin.powers)#: Trueshallow = copy.copy(goblin)shallow.powers.append("shared")print(goblin.powers) # The original changed too#: ['bite', 'shared']
Because the clone() method wraps
copy.deepcopy(), captain gets its own
powers list.
deepcopy() restores the clone’s state without
running the constructor, so a clone skips
the __post_init__() check. A prototype of a
validated type is safe because the prototype is valid, not
because anything checks the clone. When the variant differs only
in field values, copy.replace() builds it through
the constructor, as knight shows, so a
__post_init__() check runs on the result.
copy.replace()
passes every field besides name and hp
by reference, so knight shares
goblin’s powers list. Pass a fresh
list for that field when the variant must own one.
The shallow lines are a warning, not an example
to follow. copy.copy() duplicates the
Monster and shares its powers list, as
copy.replace() does for knight.
Changing that list through one object changes it for the other,
with no error to signal the sharing.
deepcopy() copies
everything it can reach, but it cannot copy an open file, a
socket, or a lock. For a prototype holding one,
deepcopy() raises a TypeError. For a
lock the message is
cannot pickle '_thread.lock' object. The message
names pickling because deepcopy() falls back on the
pickle protocol for a type that has no copy rule of its own.
Give such a class a __deepcopy__() that says what
the copy holds: a fresh connection, or an empty slot the clone
fills when it first needs one.
You can combine Prototype with a registry. Instead
of a registry of classes, keep a registry of prototypical
instances and clone the chosen one:
a.powers keeps its
one entry because spawn() calls
copy.deepcopy(), so each goblin owns its list. With
copy.copy(), both goblins share the prototype’s
list, as shallow shares goblin’s list
in prototype.py. Because
spawn() returns an independent object every time,
callers can modify their copy while the prototype stays as it
was.
Compare spawn() with make() in registry.py. There the
table holds classes and make() calls one. Here the
table holds instances and spawn() copies one. Use
the prototype form when the interesting part of an object is its
configured state rather than its type.
A prototype registry has two required properties. Each clone
must be independent, and the stored prototype must stay
unchanged:
Builder is the last of
the creational patterns. It builds a complex object in steps,
keeping the step-by-step assembly separate from the finished
object. In GoF Design Patterns a director
issues the steps to a builder through an abstract interface, so
one construction process can produce different representations.
The example in GoF Design Patterns is a document reader
that hands each token to a converter. Each converter class
produces one format (ASCII text, TeX, or an editable text
widget), so configuring the reader with a different converter
class changes the result.
The builder most programmers meet
is narrower, the one Joshua Bloch’s Effective Java
recommends. In Java, a class with many optional settings needs a
constructor for every useful combination, because Java has
neither keyword nor default arguments. That pile of constructors
is the telescoping constructor. Translated into Python,
this builder is the workaround: a companion class that collects
settings one method call at a time.
You can chain the calls because
each setter returns self. build()
freezes the accumulated settings into an immutable
Pizza.
A PizzaBuilder keeps its settings after
build() returns. build() reads
self._toppings and leaves the list in place. Every
.topping() call after the first
build() adds to that same list, so a second
build() on the same builder returns a pizza
carrying the first pizza’s toppings too. The chained call in
__main__ hides the problem, because it discards the
builder once build() returns. Making the builder
reusable means resetting the fields in build().
That reset removes the other reasonable use: configuring a
builder once and building from it twice.
Apart from that hazard, the builder class solves a problem
Python does not have. Keyword arguments with defaults are
Python’s built-in builder:
Every combination of settings is a single call. The call site
names each option just as the chain does. The fields declare the
defaults, where the builder needs a second class.
A second use for builder chains
is to vary an existing configuration. For a record,
replace() is Prototype and
Builder in one function, copying the configured state
and changing the chosen fields in the copy.
copy.replace() is the general
form of the operation, and works on any object that defines
__replace__().
# test_pizza.pyfrom dataclasses import replaceimport pizza_builder as pbimport pizza_direct as pddef test_builder_and_keywords_agree() ->None: built = (pb.PizzaBuilder() .size(16).topping("basil").build()) direct = pd.Pizza(size=16, toppings=("basil",))assert (built.size, built.cheese, built.toppings) == ( direct.size, direct.cheese, direct.toppings)def test_replace_varies_one_field() ->None: base = pd.Pizza() variant = replace(base, size=18)assert base.size ==12and variant.size ==18assert variant.toppings == base.toppingsdef test_second_build_reuses_toppings() ->None: builder = pb.PizzaBuilder().topping("basil") first = builder.build() second = builder.topping("olives").build()assert first.toppings == ("basil",)assert second.toppings == ("basil", "olives")
The (unrelated) Decorator
pattern has its own Pizza, modeling toppings as
wrapper objects instead of builder-collected fields.
Builder remains useful
in Python when construction is a process rather than a single
call. The steps must come in order, later steps depend on
earlier ones, and some rules apply across several steps.
GameBuilder in Simulation
is such a builder. It assembles a maze in three stages: creating
rooms, connecting each room to its neighbors, then pairing the
teleports that share a target letter. The second and third
stages each rely on the rooms the first stage builds. No list of
keyword arguments can express that. The standard library’s
argparse.ArgumentParser has the same shape.
add_argument() calls accumulate a specification,
and parse_args() is the build().
The smallest builder appends parts to a list and finishes
with "".join(parts), building an immutable string
through a mutable intermediate. PizzaBuilder has
the same shape. It collects toppings in a list and freezes them
into a tuple at build(). That shape is everywhere,
so save the name Builder for construction that is a
process in its own right, with intermediate state and rules that
apply across several steps. When each builder call sets one
optional value, a data class with keyword arguments sets the
same values in one call.
Which Factory to Use
Match the machinery to what varies:
When a name maps to a class, use
a dictionary. When the set of classes is closed, write out the
table and key it by a Literal, as in shape_table.py, so a bad
name fails at the check. When the set is open-ended or spread
across modules, let the classes fill the table:
__init_subclass__() on an ABC if a subclass must
register by existing, a factory object whose bounded
@make.register takes Protocol classes if the
checker should reject an incomplete class.
When the choice is which
arguments to pass, not which class, write an alternative
constructor, a @classmethod that ends with
return cls(...).
When construction does more than call a constructor
(pooling, caching, consulting configuration), write a factory
function, and a factory class only when that work has state. The
cached factory in Singleton
is one such function.
When a class does work that needs a product and each
subclass picks the product, override a factory method, as
CircleSketch and SquareSketch do in shape_sketch.py.
When you must choose several
products together as a matched set, use Abstract
Factory, expressed as a Protocol rather than a
base class.
When the interesting part of an
object is its configured state rather than its type, keep a
prototype and copy it. For a record, replace() is
that copy.
When construction is a process
with ordered steps and rules that apply across several of them,
use Builder. When each builder call sets one optional
value, keyword arguments are the builder.
The static factory() method and the factory
objects in nested Factory classes are here because
the object-oriented tradition writes factories that way, not
because Python needs them. Both exist to work around languages
where a class is not an object you can put in a dictionary.
Add a new type of GameElementFactory called
GnomesAndFairies, first to abstract_factory_abc.py
and then to abstract_factory_protocol.py.
In abstract_factory_protocol.py,
leave out make_obstacle() at first, pass the
factory to GameEnvironment, and confirm the error
your type checker reports. Then add
make_obstacle().
Modify shape_factory_objects.py
to use an Abstract Factory to create different sets of
shapes (for example, one type of factory object creates “thick
shapes,” another creates “thin shapes,” but each factory object
can create all the shapes: circles, squares, triangles,
etc.).
Add a rule to both pizza examples: a pizza may carry at most
four toppings. In pizza_direct.py, enforce
it with __post_init__(), as Data
Classes as Types does for Stars. In pizza_builder.py, decide
whether it belongs in topping() or
build(). In which version can an invalid pizza
exist, even momentarily? stars_class.py in that
chapter shows the same hazard.
Move Circle and Square out of registry.py into a new
module, extra_shapes.py. Confirm that
make("Circle") now raises KeyError
until something imports extra_shapes, and explain
which line of which file registers the class, and when it runs.
Then make registry_demo.py print the
same key list it printed before the move.
Give Monster in prototype_registry.py a
parts: dict[str, int] field and add a prototype
that uses it. Change spawn() to use
copy.copy() instead of
copy.deepcopy(), run test_prototype.py with
pytest
(uv run pytest Examples/26_Patterns--Factory/test_prototype.py
from the repository root), and explain which assertions fail and
why. Then restore deepcopy() and add a test that
would have caught the bug through parts rather than
powers.
Recreate the eval() factory described after shape_factory_objects.py’s
listing: a create_shape() that builds each factory
with eval(f"_{kind}.Factory()") instead of
consulting FACTORIES. Call it with a
kind string that is not a shape name but a Python
expression with a side effect, and show that it runs the
expression. Then show that the FACTORIES version
raises KeyError for the same string.
Derive _Oval from _Circle in shape_factory_method.py,
give it its own draw(), and add a
case "Oval" to factory(). Run the
program and confirm that shape_name() never yields
"Oval", then explain why. Write a recursive
generator all_subclasses() that yields a class’s
direct subclasses and, through each one’s own
__subclasses__(), every class below them. Use it in
shape_name() and confirm that Oval now
appears.
Add a Hexagon to protocol_registry.py that
satisfies Shape but carries no
@make.register, and show what
make("Hexagon") does. Then write a check that
reports every class in the module that satisfies
Shape and is missing from
make.registry, so you find the forgotten decorator
before any make() call.
@runtime_checkable, which Surrogate
shows with isinstance(), also lets
issubclass() test a class against a Protocol whose
members are all methods.
Fill PROTOTYPES in prototype_registry.py by
decoration instead of a table literal. Write a
@prototype(name) decorator for a function that
builds and returns the Monster, so that the
decorator stores each decorated function’s result under
name. Explain why the decorator takes the name as
an argument rather than reading the function’s
__name__. Write that version and read what
ty reports. Then say what the decorated form gains
over the table and what it loses.
You are visitor 01402503 — 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.