flyweight¶
Intern instances by their constructor arguments, so equal arguments always yield the same object.
When to reach for it¶
Reach for flyweight when many parts of your program ask for the same logical thing and you
want them to share one immutable instance. Colors, currencies, glyphs, units, parsed schemas,
anything that is value-like and expensive or wasteful to rebuild. The same arguments give you
the same object, built once and shared forever.
The anti-pattern it replaces¶
It replaces @cache slapped over __init__, which does not actually work, since __init__
returns None and the cache caches nothing useful while the object is still rebuilt. It also
replaces ad-hoc interning dicts scattered across the codebase. flyweight moves interning into
the type, keeps instances immutable, and builds each one exactly once.
Usage¶
FlyweightMeta is a metaclass. Set it on the class you want interned, and construction caches by
the constructor arguments.
from patos import FlyweightMeta
class Color(metaclass=FlyweightMeta):
def __init__(self, name: str) -> None:
self.name = name
a = Color("teal")
b = Color("teal")
c = Color("amber")
assert a is b # same args, same interned object
assert a is not c
Because FlyweightMeta composes with ABCMeta, a flyweight can also be abstract.
from abc import abstractmethod
from patos import FlyweightMeta
class Shape(metaclass=FlyweightMeta):
@abstractmethod
def area(self) -> float: ...
Public API¶
FlyweightMeta. The metaclass that performs interning. Use it asclass X(metaclass=FlyweightMeta). The first construction with a given(args, kwargs)builds and caches the instance, and every later construction with equal arguments returns that same object without re-running__init__. Each class keeps its own cache and arguments must be hashable. It composes withABCMeta, so a flyweight can also be an abstract base class. An argument whose equality is not a plainbool(a model carrying tensors, wherea == breduces a tensor and raises) is interned by identity rather than value, so the same object still shares one instance while two equal-but-distinct such arguments get separate instances rather than crashing the lookup.
Source¶
Copy this into your project and own it. No dependency, no tool, just one module you can read and change.
from abc import ABCMeta
from typing import TypeVar, cast
_ResultT = TypeVar("_ResultT")
# Named rather than written inline as `except (RuntimeError, ValueError, TypeError):`, because
# ruff targets 3.14 for the `sql` extra and its formatter rewrites an inline tuple into PEP 758's
# unparenthesized form, which the 3.13 core floor cannot parse. A single name is the one spelling
# both interpreters accept and no formatter rewrites. These are what an argument raises when its
# `==` refuses to answer, a tensor-carrying model among them.
_UNRESOLVABLE_EQUALITY = (RuntimeError, ValueError, TypeError)
class Arg:
"""One flyweight key element: hashes by its argument and compares without a tensor `__eq__`.
The flyweight interns by argument value, but an argument whose `==` does not return a plain
`bool` -- a pydantic model carrying tensors reduces a tensor to a bool and raises, a raw tensor
returns an elementwise mask -- cannot key a plain `dict`, whose lookup would raise on the
collision compare. Resolving a hash collision by identity first, then a guarded value compare
that treats an unresolvable equality as distinct, keeps interning exact for well-behaved
arguments and degrades to per-object for one whose equality is undefined, never crashing.
Typing stays like `lru_cache(typed=True)`: the argument's type joins the hash and gates the
compare, so `Arg(1)`, `Arg(True)` and `Arg(1.0)` are distinct keys.
"""
__slots__ = ("kind", "value")
def __init__(self, value: object) -> None:
self.kind = type(value)
self.value = value
def __hash__(self) -> int:
return hash((self.kind, self.value))
def __eq__(self, other: object) -> bool:
if not isinstance(other, Arg) or self.kind is not other.kind:
return False
if self.value is other.value:
return True
try:
return self.value == other.value
except _UNRESOLVABLE_EQUALITY:
return False
CacheKey = tuple[tuple[Arg, ...], frozenset[tuple[str, Arg]]]
class FlyweightMeta(ABCMeta):
"""Metaclass that interns instances by their construction arguments.
The first construction with a given `(args, kwargs)` builds and caches the instance; every
later construction with equal arguments returns that same object without re-running
`__init__` -- the metaclass owns `__call__`, so no re-entrancy guard is needed (unlike
caching `__new__`, which still re-runs `__init__`). Each class keeps its own cache and
arguments must be hashable. Interning is typed like `lru_cache(typed=True)`, so `Node(1)`,
`Node(True)` and `Node(1.0)` stay distinct instances even though the arguments compare
equal. Subclasses `ABCMeta` so a flyweight can also be an abstract base or carry abstract
methods.
"""
# `cls: type[_ResultT]` makes `Node(...)` return `Node`. Construction args stay `object`
# because one metaclass serves every class, each with its own `__init__` signature.
def __call__(cls: type[_ResultT], *args: object, **kwargs: object) -> _ResultT:
cache: dict[CacheKey, object] = cls.__dict__.get("flyweights", {})
if "flyweights" not in cls.__dict__:
type.__setattr__(cls, "flyweights", cache)
key = (
tuple(map(Arg, args)),
frozenset((name, Arg(value)) for name, value in kwargs.items()),
)
if key not in cache:
cache[key] = type.__call__(cls, *args, **kwargs)
return cast(_ResultT, cache[key])