A decorator is a function that takes a function and returns a replacement for it. The @ line is shorthand for reassigning the name.
Once you have written one by hand, the syntax stops looking like magic.
Write one without the @
The inner wrapper takes anything, calls the original, and changes the result.
# a decorator is a function that returns a function
def shout(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs).upper()
return wrapper
def greet(name):
return f'hello {name}'
loud = shout(greet)
print(loud('ada'))
It prints:
HELLO ADA
Nothing special has happened yet. A function was passed in and a different function came out.
The @ line is the same thing
@shout above a definition means greet2 = shout(greet2).
# the @ syntax is the same thing
@shout
def greet2(name):
return f'hello {name}'
print(greet2('ada'))
It prints:
HELLO ADA
Same result. The decorator runs once, at definition time.
The wrapper hides the original
The name and the docstring now belong to the wrapper.
# the wrapper hides the original function
print(greet2.__name__, '|', greet2.__doc__)
It prints:
wrapper | None
This breaks help output, debuggers and anything that reads __name__.
functools.wraps fixes it
@functools.wraps(fn) copies the name, docstring and a few other attributes onto the wrapper.
# functools.wraps keeps the metadata
import functools
def shout_fixed(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs).upper()
return wrapper
@shout_fixed
def greet3(name):
"""Say hello."""
return f'hello {name}'
print(greet3.__name__, '|', greet3.__doc__)
print(greet3('ada'))
It prints:
greet3 | Say hello.
HELLO ADA
Add it to every decorator you write. There is no case where you want the wrapper metadata instead.
A decorator that takes arguments
@repeat(3) means “call repeat(3), then use what it returns as the decorator”. That needs one more level of nesting.
# a decorator that takes arguments needs one more layer
def repeat(times):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return [fn(*args, **kwargs) for _ in range(times)]
return wrapper
return decorator
@repeat(3)
def roll():
return 4
print(roll())
It prints:
[4, 4, 4]
Three layers: the argument taker, the decorator, the wrapper.
Keeping state
The wrapper is a closure, so it can hold state between calls. Attaching it to the wrapper makes it readable from outside.
# state in the closure, such as counting calls
def counted(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return fn(*args, **kwargs)
wrapper.calls = 0
return wrapper
@counted
def work(n):
return n * 2
work(1); work(2); work(3)
print('called', work.calls, 'times')
It prints:
called 3 times
The one in the standard library
functools.lru_cache is a decorator that caches results by arguments. On a naive recursive Fibonacci it turns exponential work into linear.
# caching is a decorator in the standard library
@functools.lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(30), fib.cache_info())
It prints:
832040 CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)
The cache info shows how many calls were answered without running the body.
What to remember
@decoratormeansname = decorator(name).- The decorator runs at definition time, the wrapper runs at call time.
- Always use
functools.wrapsor you lose the name and docstring. - A decorator with arguments needs three nested functions.
Most decorators you will write are logging, timing, caching, retrying or access checks. All of them are the same shape as the first example on this page.