FunctionsChapter 56 of 114
Decorators
Wrap a function to add behaviour, without editing the function.
A function that wraps a function
A decorator takes a function and returns a replacement. Since functions are ordinary values, nothing new is needed to write one:
def loud(fn):
def wrapper(*args, **kwargs):
print("calling", fn.__name__)
result = fn(*args, **kwargs)
print("done")
return result
return wrapper
def add(a, b):
return a + b
add = loud(add)
print(add(2, 3))Output
calling add done 5
The @ is shorthand
@loud above a def means exactly add = loud(add):
def loud(fn):
def wrapper(*args, **kwargs):
print("calling", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@loud
def add(a, b):
return a + b
print(add(2, 3))Output
calling add 5
The *args, **kwargs matter: the wrapper has to accept whatever the original did, and pass it straight through.
It hides the original
The name and docstring now belong to the wrapper, which breaks help() and confuses tracebacks:
def loud(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@loud
def add(a, b):
"""Add two numbers."""
return a + b
print(add.__name__)
print(add.__doc__)Output
wrapper None
functools.wraps copies the metadata across. Always use it:
from functools import wraps
def loud(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@loud
def add(a, b):
"""Add two numbers."""
return a + b
print(add.__name__)
print(add.__doc__)Output
add Add two numbers.
A decorator that takes arguments
This needs one more layer: a function that returns a decorator.
from functools import wraps
def repeat(times):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet():
print("hello")
greet()Output
hello hello hello
Read @repeat(3) as: call repeat(3), which gives back a decorator, then apply that. Three layers looks like a lot, and it is the standard shape.
Stacking
They apply bottom up — the one nearest the def wraps first:
from functools import wraps
def tag(name):
def decorator(fn):
@wraps(fn)
def wrapper():
return f"<{name}>{fn()}</{name}>"
return wrapper
return decorator
@tag("b")
@tag("i")
def text():
return "hi"
print(text())Output
<b><i>hi</i></b>
Ones you already use
from functools import lru_cache
@lru_cache
def slow_square(n):
return n * n
print(slow_square(4))
print(slow_square(4))
print(slow_square.cache_info().hits)Output
16 16 1
@property, @staticmethod, @classmethod and @dataclass are all decorators too. You have been using them since the classes chapters.
Keep them boring
A decorator runs on every call, and it is invisible at the call site. That makes it perfect for cross-cutting concerns — timing, caching, logging, retries, access checks — and a poor place for anything a reader needs to know about to understand what the function returns.
Test yourself
2 questionsWhat does @loud above a def actually do?
Show the answer
Replaces the function with loud(function) — The @ is only shorthand for a reassignment. Nothing magic is happening.
Why use functools.wraps inside a decorator?
Show the answer
It copies the original's name and docstring onto the wrapper — Without it, help(), tracebacks and introspection all show the wrapper instead.
Generators
Produce values one at a time instead of building a whole list.