Closures & late binding showed a nested
function reaching into its enclosing scope. A decorator is the single most common thing built that way:
@deco above a def is nothing more than f = deco(f), run once, right
after the function is defined, and the closure it creates is what remembers the original f
every time the wrapper is called. Everything below — arguments, stacking, lru_cache — is that
one idea, layered.
Each ring below is one decorator's wrapper, wrapped around the layer inside it; the original function sits at the centre. Calling the decorated name calls the OUTERMOST ring first — the call travels inward, layer by layer, until it reaches the real function, and the return value travels back outward through every layer it passed through, in reverse.
f = deco(f), not magic@deco written above a def desugars to exactly one thing: define the function, then
immediately call f = deco(f) and rebind the name to whatever deco returns. Nothing
runs later that isn't already implied by that line — the "magic" is entirely in deco choosing to
return a different, wrapping function instead of f itself.
functools.wraps is not cosmeticA wrapper function is a perfectly ordinary function with its own __name__
("wrapper"), its own __doc__ (None, unless you wrote one), and no idea
it's standing in for something else. Anything that introspects a function to do its job — help(),
API doc generators, logging that prints fn.__name__, pickle trying to find the
function by its qualified name — now sees the wrapper's identity instead of the real one.
functools.wraps(f) copies __name__, __doc__, __module__
and more from f onto the wrapper, and also sets wrapper.__wrapped__ = f so tooling
can still find the original underneath.
@repeat(3) is not itself a decorator — repeat(3) is a plain function call that
returns one. repeat is a factory: it takes the arguments, and returns the actual
decorator, which then takes f and returns the wrapper. Three calls, three functions, one extra
ring around everything you've already seen.
@A above @B above def f applies B first —
f = A(B(f)) — which makes A's wrapper the OUTERMOST ring. Read bottom-up for
application order, top-down for execution order: A's wrapper is what actually
runs first when you call the decorated name, and its return is the last thing to happen.
@deco means f = deco(f), run once. A
wrapper is a real function with its own identity — functools.wraps is what makes it keep
pretending to be the original for every tool that asks. A decorator "with arguments" is a factory: one more
ring, built by a plain function call before the real decoration happens. Stacked decorators apply
bottom-up but the outermost one runs first and returns last. functools.lru_cache is a decorator
like any other — a ring that remembers past calls instead of re-running the one inside it.