What the @ line actually means
In plain words
You wrote a function. Someone hands it to a helper and says "give me back a better version". The better version goes under the old name, and your original goes in the helper's pocket. That's a decorator. The @ symbol is just a shorter way to write the handover.
To be precise, @deco above def f compiles to f = deco(f). The def runs first and produces a function object, that object is passed to deco, and the name f is bound to whatever comes back.
The def runs
A function object appears on the heap. It has no name yet, because the decorator line hasn't finished.
The decorator is called
Your function object goes in as an argument. Whatever comes out is usually a new function.
The name is assigned
The name in your module now points at the returned object. Session 01's rule, applied to a function.
decorator type in Python. Anything callable that accepts one argument works: a function, a class, an object with __call__. The @ is punctuation, not machinery.
Define def shout(f): ... and a plain def greet(name): ...,
then run greet = shout(greet) by hand. Compare id(greet)
before and after. You just decorated a function without typing @ once.
Where your original function goes
In plain words Nothing is thrown away. Your function is still there, at the same address it always had. What changed is that no name points at it any more. It's reachable only through the wrapper that swallowed it.
The wrapper uses func, a name from the enclosing call. That makes func a free variable, so Python attaches a closure cell to the wrapper holding a reference to your function. The decorator's frame is discarded when it returns. The cell survives because it belongs to the wrapper.
The name
greet was pointing at your function. Now it points at the wrapper. Nothing was copied or edited.
The object
Your function still exists at the same address, with its refcount held up by the closure cell alone.
The wrapper wears the wrong name tag
In plain words
You asked for a coat and got the coat back with someone else's name sewn in.
Your function still runs, but every tool that reads its label (help(), a traceback, a log line) now reports the wrapper's name instead of yours.
functools.wraps(original) is a decorator you apply to the wrapper.
It copies __name__, __qualname__, __doc__,
__module__ and __annotations__ from the original onto
the wrapper, updates __dict__, and sets __wrapped__ to
the original object so tools can unwrap it.
| You call | Without wraps |
With wraps |
|---|---|---|
greet.__name__ |
'wrapper' |
'greet' |
greet.__doc__ |
None |
Your docstring |
greet.__wrapped__ |
AttributeError |
The undecorated function |
greet("ada") |
'HI ADA' |
'HI ADA', the behaviour is identical |
wraps is a debugging tax you pay laterwrapper in every decorated function in your codebase, and they all look the same.
Decorate a function twice, once with @functools.wraps(func) on the wrapper and once without, then compare help() on each. The difference is what your future self will be reading.
Why @repeat(2) needs one more def
In plain words
The @ line only ever passes your function to one thing. If you also want to pass a number, you need something that takes the number first and gives back the thing that will take your function. That's the third layer.
@repeat(2) evaluates repeat(2) immediately, before any decorating happens, and the result is what gets applied to your function. So repeat is a decorator factory, not a decorator. Two calls happen at import time, and your function body still runs zero times.
repeat(2) runs. Returns decorator, which remembers times = 2.
decorator
decorator(ping) runs. Returns wrapper, which remembers func and times.
wrapper
ping is pointed at wrapper.
ping
@deco means deco receives your function.
@deco() means deco() is called first and its result
receives your function. Mixing these up is the most common decorator bug there is, and the error message is usually a baffling TypeError about the wrong number of arguments.
Two decorators, two different orders
In plain words Think of wrapping a parcel. The layer you put on first ends up innermost, and the last layer you add is the one someone opens first. Decorators are applied from the bottom up and entered from the top down, for the same reason.
@bold above @italic above def text compiles to
text = bold(italic(text)). The inner call happens first, so
italic is applied first and ends up nested inside bold's
wrapper. Calling text() then enters bold's wrapper first.
Applied bottom-up
italic(text) runs, then bold(...) runs on its result.
The decorator nearest the def gets your real function.
Executed top-down
text() enters bold's wrapper, which calls italic's wrapper,
which calls your function. Result: <b><i>hi</i></b>.
A name, an outermost wrapper, a closure cell inside it holding the next wrapper, and your original function at the bottom of the chain. If you can draw that, you can predict what any stack of decorators does, and where to look when one of them breaks.
Four ways this goes wrong
Forgetting to return the wrapper
A decorator that defines wrapper but never returns it returns
None. Your name is then bound to None, and the first
call gives TypeError: 'NoneType' object is not callable. If you see
that error right after adding a decorator, check its last line.
A wrapper that only accepts the arguments you tested with
def wrapper(text) works until someone decorates a two-argument
function. Write def wrapper(*args, **kwargs) and pass them through
with func(*args, **kwargs) unless you have a reason not to.
Leaving out functools.wraps
Behaviour is fine; everything that reads a function's labels is not.
Tracebacks, help(), API docs and log lines all start reporting
wrapper. One line fixes it permanently.
Writing @deco() when the decorator takes no arguments
That calls deco with nothing, then applies whatever it returned
to your function. If deco expected a function, you get a TypeError at import time, before any of your code runs. The reverse, @repeat without (2), binds your function to the wrong layer without a word.
None, you forgot a return. If it's your original function, the decorator didn't wrap. If it's a wrapper with the wrong signature, the *args are missing.