Decorators I: what @ really does
Part of the free LogicWiz Python course, Module 11: Functions in depth. The examples and practice run in the app.
Canteen Counter has twelve functions, and Shankar bhaiya wants every call logged. Aarav could edit all twelve, or write one decorator and put @log_calls above each.
You'll learn to
- Explain that `@deco` above a def means `f = deco(f)`
- Write decorators like `@log_calls` and `@timer` that work for any arguments
- Keep the function's name and docstring with `functools.wraps`
- Predict the order of stacked decorators
Learn
A decorator is a function that wraps a function
A decorator takes a function and returns a new function, usually one that does something extra before or after calling the original.
You already have both pieces:
- functions can be passed in and returned (Lesson 11.2)
*args, **kwargsforward any arguments (Lesson 11.1)
Let's build one by hand first, with no @.
Wrapping by hand
def shout(fn):
def wrapper():
return fn().upper() + "!"
return wrapper
def greet():
return "namaste"
greet = shout(greet) # replace greet with the wrapped one
print(greet())
After the swap, the name greet points at wrapper, and wrapper remembers the old greet as fn.
@ is just a shortcut
These two are exactly the same:
@shout
def greet():
return "namaste"
def greet():
return "namaste"
greet = shout(greet)
The decorator runs once, when the def runs. The wrapper runs on every call. To work with any function, the wrapper takes *args, **kwargs, passes them on, and returns the result.
@log_calls
def log_calls(fn):
def wrapper(*args, **kwargs):
print(f"calling {fn.__name__}{args}")
result = fn(*args, **kwargs)
print(f"{fn.__name__} returned {result}")
return result
return wrapper
@log_calls
def item_total(price, qty):
return price * qty
total = item_total(15, 4)
print("total is", total)
@timer
import time
from functools import wraps
def timer(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} took {elapsed:.4f} s")
return result
return wrapper
@timer
def daily_total(n):
return sum(i * 15 for i in range(n))
print(daily_total(200000))
The time is different on every run and every phone, so we never ask you to predict it.
Why @wraps matters
from functools import wraps
def plain(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
def careful(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@plain
def gst(amount):
"Add 5% GST."
return amount * 1.05
@careful
def tip(amount):
"Add a 10% tip."
return amount * 1.1
print(gst.__name__, gst.__doc__)
print(tip.__name__, tip.__doc__)
Stacking: nearest decorator wraps first
def bold(fn):
def wrapper():
return "<b>" + fn() + "</b>"
return wrapper
def italic(fn):
def wrapper():
return "<i>" + fn() + "</i>"
return wrapper
@bold
@italic
def menu_title():
return "Today's Specials"
print(menu_title()) # menu_title = bold(italic(menu_title))
Two classic slips:
- Forgetting
return resultin the wrapper. The decorated function then returnsNone. - Forgetting
@wraps(fn). The function's name becomeswrapperand its docstring disappears, which breakshelp(), logs and test reports.
"What is a decorator? Write a timer decorator." is one of the most asked Python interview questions. Say it in one line: a decorator is a function that takes a function and returns a new function that adds behaviour; @d above def f means f = d(f). Then write @timer with *args, **kwargs and @wraps.
In one line
@d above def f means f = d(f); the wrapper takes *args, **kwargs, returns the result, and uses @wraps(fn); stacked decorators apply bottom-up and run top-down.
Interview corner
What is a decorator in Python?
A decorator is a callable that takes a function and returns a new function adding behaviour around it, without changing its code. @timer above def f is just f = timer(f), run once at definition time. Typical uses: logging, timing, caching, access checks, retries.
Write a decorator that times a function.
import time
from functools import wraps
def timer(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
print(fn.__name__, time.perf_counter() - start)
return result
return wrapper
Mention *args, **kwargs (works for any signature), return result, and @wraps.
Why use functools.wraps?
Without it the decorated function takes the wrapper's identity: __name__ becomes "wrapper" and the docstring is lost. @wraps(fn) copies __name__, __doc__, __module__, __qualname__ and __dict__, and sets __wrapped__ to the original, so help(), logging, debuggers and test tools keep working.
In what order do stacked decorators run?
They are applied bottom-up: @a @b def f is f = a(b(f)). When called, the outermost wrapper (a) runs first and calls b's wrapper, which calls f. So code before the call runs top-down and code after it runs bottom-up.