Decorators
A track of P29 · Functions, Decorators & Generators.
From @sugar to design tool: hand-write wrappers with functools.wraps, parametrise them as three-layer factories, stack them deliberately, and write class-based decorators with __call__.
Your team's codebase has @app.route("/orders"), @lru_cache(maxsize=256),
@pytest.mark.parametrize, @retry(3) — and one day a stack of two of them
misbehaves: the retry logs three attempts in production but your @log above
it shows only one call. Which decorator is wrapping which? If you read @ as
an incantation, that bug is a wall. If you read it as what it is — a function
call at def time — it is a one-line diagnosis.
That reading is this track's whole method. @a above a function executes
f = a(f), once, when the def runs; the decorator receives a function and
returns a replacement, usually a closure called wrapper that forwards
*args, **kwargs, does its extra work, and hands the result back. You write
@count_calls and @memoize from scratch — the hand-rolled heart of
lru_cache — and learn the one-line habit that keeps decorated functions
debuggable: functools.wraps, so the wrapper stops erasing the name,
docstring and signature of what it wraps.
The second module adds the two production shapes. @retry(3) has parentheses
— so retry(3) runs first and must return a decorator: a factory, three
nested layers, settings captured in the closure. Stacking applies bottom-up,
which is why log-outside-retry and retry-outside-log are different systems —
the opening bug, resolved by reading order. And when wrapper state outgrows a
counter, a class with __init__ + __call__ decorates just as well, with
instance attributes for state — plus the method subtlety interviewers love:
the wrapper sees self as merely its first positional argument. The track
closes with a free-form interview rung that asks you to explain the whole
machine end to end.