Lesson 4 of 10
You met @property in lesson 5.8. A decorator is a function that takes a function and
returns a new one wrapping it — a way to add behaviour to something without editing it.
It builds on lesson 4.7: functions are values you can pass around and return.
Building one from scratch
def shout(func):
"""Wrap func so its result is upper-cased."""
def wrapper():
result = func()
return result.upper()
return wrapper
def greet():
return "hello"
greet = shout(greet)
print(greet())
shout takes a function, defines a new function that calls it and modifies the result, and
returns that. Reassigning greet swaps the original for the wrapped version.
The @ syntax
@shout above a definition means exactly greet = shout(greet):
def shout(func):
def wrapper():
return func().upper()
return wrapper
@shout
def greet():
return "hello"
print(greet())
Same thing, less noise.
Handling arguments
A decorator that only works on zero-argument functions is nearly useless. *args and **kwargs
from lesson 4.4 make it universal:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name, greeting="hello"):
return f"{greeting}, {name}"
print(greet("ada"))
print(greet("grace", greeting="hi"))
wrapper accepts anything and forwards it unchanged.
functools.wraps
Wrapping hides the original's identity:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet():
"""Say hello."""
return "hello"
print(greet.__name__)
print(greet.__doc__)
The name and docstring now belong to wrapper, which breaks help() and confuses debuggers.
functools.wraps copies them across:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet():
"""Say hello."""
return "hello"
print(greet.__name__)
print(greet.__doc__)
Always use functools.wraps. It's one line and it prevents real confusion later.
A practical example
Logging calls without touching the functions:
import functools
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}{args}")
result = func(*args, **kwargs)
print(f" -> {result}")
return result
return wrapper
@logged
def add(a, b):
return a + b
add(2, 3)
Common real uses: timing, caching, retries (lesson 8.5), access checks, and input validation.
Stacking
Decorators apply bottom-up — the one closest to the function wraps first:
import functools
def bold(func):
@functools.wraps(func)
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def text():
return "hi"
print(text())
italic wraps text, then bold wraps that — so <b> ends up outermost.
Built-in decorators
functools.lru_cache memoises results, which turns the slow recursive Fibonacci from lesson 4.8
into a fast one:
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30))
print(fib.cache_info().hits > 0)
Without the cache, fib(30) makes over a million calls. With it, each n is computed once.
Try it yourself
Checkpoint
- A decorator takes a function and returns a wrapped replacement.
@decoratoris shorthand forfunc = decorator(func).- Use
*args, **kwargsin the wrapper so it works with any signature. - Always apply
@functools.wraps(func)to preserve name and docstring. - Stacked decorators apply bottom-up;
lru_cacheis a ready-made memoiser.
Questions about this lesson
Log in to ask a question.