Decorators
Decorators Introduction
You need to add logging to 50 functions, or timing to every API endpoint, or authentication checks everywhere. Instead of modifying each function, decorators let you wrap functions with reusable behavior - just add @log or @timed above any function definition.
A decorator is a function that wraps another function (or class) to add behavior without modifying the original code.
Simple Decorator
simple.py
Replay: real traced execution (multi-file project)
# Simple decorator
def shout_decorator(func):
def wrapper():
print("BEFORE!")
func()
print("AFTER!")
return wrapper
message = "hello"
# Decorate with @
@shout_decorator
def greet():
print(message)
greet()
# Simple decorator
def shout_decorator(func):
def wrapper():
print("BEFORE!")
func()
print("AFTER!")
return wrapper
message = "hi"
# Decorate with @
@shout_decorator
def greet():
print(message)
greet()
# Simple decorator
def shout_decorator(func):
def wrapper():
print("BEFORE!")
func()
print("AFTER!")
return wrapper
message = "welcome"
# Decorate with @
@shout_decorator
def greet():
print(message)
greet()
message ← hello
13message→ hello = "hello"14#@message="hi", "welcome"def shout_decorator(func):
4def shout_decorator(func⟨function greet A⟩):5 def wrapper():6 print("BEFORE!")7 func()8 print("AFTER!")910 return wrapper<function shout_decorator.<locals>.wrapper at ⟨addr B⟩>greet()
22greet()def wrapper():
4def shout_decorator(func):5 def wrapper():6 print("BEFORE!")7 func()8 print("AFTER!")outputBEFORE!def greet():
6 print("BEFORE!")7 func()8 print("AFTER!")910 return wrapper111213message = "hello"14#@message="hi", "welcome"1516# Decorate with @17@shout_decorator18def greet():19 print(messagehello)outputhello AFTER!greet()
22greet()
message ← hi
13message→ hi = "hi"def shout_decorator(func):
4def shout_decorator(func⟨function greet A⟩):5 def wrapper():6 print("BEFORE!")7 func()8 print("AFTER!")910 return wrapper<function shout_decorator.<locals>.wrapper at ⟨addr B⟩>greet()
21greet()def wrapper():
4def shout_decorator(func):5 def wrapper():6 print("BEFORE!")7 func()8 print("AFTER!")outputBEFORE!def greet():
6 print("BEFORE!")7 func()8 print("AFTER!")910 return wrapper111213message = "hi"1415# Decorate with @16@shout_decorator17def greet():18 print(messagehi)outputhi AFTER!greet()
21greet()
message ← welcome
13message→ welcome = "welcome"def shout_decorator(func):
4def shout_decorator(func⟨function greet A⟩):5 def wrapper():6 print("BEFORE!")7 func()8 print("AFTER!")910 return wrapper<function shout_decorator.<locals>.wrapper at ⟨addr B⟩>greet()
21greet()def wrapper():
4def shout_decorator(func):5 def wrapper():6 print("BEFORE!")7 func()8 print("AFTER!")outputBEFORE!def greet():
6 print("BEFORE!")7 func()8 print("AFTER!")910 return wrapper111213message = "welcome"1415# Decorate with @16@shout_decorator17def greet():18 print(messagewelcome)outputwelcome AFTER!greet()
21greet()
decorator - a function that takes a function and returns a modified version, applied with @decorator syntax
Decorator Equivalence
The @decorator syntax is equivalent to:
def func():
...
func = decorator(func)
Common Use Cases
use_cases.py
Replay: real traced execution (multi-file project)
# Common use-cases
import time
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}({args}, {kwargs})")
return func(*args, **kwargs)
return wrapper
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = 1000.0
result = func(*args, **kwargs)
elapsed = 1000.01 - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
# Apply decorators
@log_calls
def add(a, b):
return a + b
@timing
def slow_task():
time.sleep(0.01)
return "done"
print(add(2, 3))
print(slow_task())
def log_calls(func):
7def log_calls(func⟨function add A⟩):8 @wraps(func)9 def wrapper(*args, **kwargs):10 print(f"calling {func.__name__}({args}, {kwargs})")11 return func(*args, **kwargs)1213 return wrapper⟨function add B⟩def timing(func):
16def timing(func⟨function slow_task C⟩):17 @wraps(func)18 def wrapper(*args, **kwargs):19 start = 1000.020 result = func(*args, **kwargs)21 elapsed = 1000.01 - start22 print(f"{func.__name__} took {elapsed:.4f}s")23 return result2425 return wrapper⟨function slow_task D⟩print(add(2, 3))
40print(add(2, 3))41print(slow_task())def wrapper(*args, **kwargs):
8@wraps(func)9def wrapper(*args(2, 3), **kwargs):10 print(f"calling {func.__name__add}({args(2, 3)}, {kwargs{}})")11 return func(*args(2, 3), **kwargs{})outputcalling add((2, 3), {})def add(a, b):
29@log_calls30def add(a2, b3):31 return a2 + b3print(add(2, 3))
40print(add(2, 3))41print(slow_task())output5start ← 1000.0
17@wraps(func)18def wrapper(*args(), **kwargs):19 start→ 1000.0 = 1000.020 result = func(*args(), **kwargs{})21 elapsed = 1000.01 - startdef slow_task():
34@timing35def slow_task():36 time<module 'time' (built-in)>.sleep(0.01)37 return "done"result ← done, elapsed ← 0.009999999999990905
19start = 1000.020result→ done = func(*args(), **kwargs{})21elapsed→ 0.009999999999990905 = 1000.01 - start1000.022print(f"{func.__name__slow_task} took {elapsed0.009999999999990905:.4f}s")23return resultdoneoutputslow_task took 0.0100sprint(slow_task())
40print(add(2, 3))41print(slow_task())outputdone
- Logging: print when a function is called
- Timing: measure execution time
- Caching: memoize results
- Authentication: check permissions before running
- Validation: ensure inputs are correct
Multiple Decorators
multiple.py
Replay: real traced execution (multi-file project)
# Multiple decorators
def upper(func):
def wrapper():
result = func()
return result.upper()
return wrapper
def exclaim(func):
def wrapper():
result = func()
return result + "!"
return wrapper
# Bottom to top application
@upper
@exclaim
def greet():
return "hello"
print(greet())
# Equivalent to
def greet2():
return "hello"
greet2 = upper(exclaim(greet2))
print(greet2())
def exclaim(func):
pass 1 of 212def exclaim(func⟨function greet A⟩):13 def wrapper():14 result = func()15 return result + "!"1617 return wrapper<function exclaim.<locals>.wrapper at ⟨addr B⟩>def upper(func):
pass 1 of 24def upper(func<function exclaim.<locals>.wrapper at ⟨addr B⟩>):5 def wrapper():6 result = func()7 return result.upper()89 return wrapper<function upper.<locals>.wrapper at ⟨addr C⟩>print(greet())
27print(greet())result ← hello
13def wrapper():14 result→ hello = func()15 return resulthello + "!"result ← hello!
5def wrapper():6 result→ hello! = func()7 return resulthello!.upper()greet2 = upper(exclaim(greet2))
27print(greet())2829# Equivalent to30def greet2():31 return "hello"323334greet2 = upper(exclaim(greet2⟨function greet2 D⟩))35print(greet2())outputHELLO!def exclaim(func):
pass 2 of 212def exclaim(func⟨function greet2 D⟩):13 def wrapper():14 result = func()15 return result + "!"1617 return wrapper<function exclaim.<locals>.wrapper at ⟨addr E⟩>def upper(func):
pass 2 of 24def upper(func<function exclaim.<locals>.wrapper at ⟨addr E⟩>):5 def wrapper():6 result = func()7 return result.upper()89 return wrapper<function upper.<locals>.wrapper at ⟨addr F⟩>greet2 ← <function upper.<locals>.wrapper at ⟨addr F⟩>
34greet2→ <function upper.<locals>.wrapper at ⟨addr F⟩> = upper(exclaim(greet2))35print(greet2())result ← hello
13def wrapper():14 result→ hello = func()15 return resulthello + "!"result ← hello!
5def wrapper():6 result→ hello! = func()7 return resulthello!.upper()print(greet2())
34greet2 = upper(exclaim(greet2))35print(greet2())outputHELLO!
decorator stacking - applying multiple decorators, executed from bottom to top
Execution Order
Applied from bottom to top: decorator1(decorator2(func)).
execution_order.py
Replay: real traced execution (multi-file project)
# Decorator execution order
def trace(func):
print(f"decorating {func.__name__}")
def wrapper():
print(f"calling {func.__name__}")
func()
return wrapper
# Decorator runs at definition time
print("defining function...")
@trace
def hello():
print("hello from function")
print("calling function...")
hello()
print("defining function...")
14# Decorator runs at definition time15print("defining function...")outputdefining function...def trace(func):
4def trace(func⟨function hello A⟩):5 print(f"decorating {func.__name__hello}")67 def wrapper():8 print(f"calling {func.__name__}")9 func()1011 return wrapper<function trace.<locals>.wrapper at ⟨addr B⟩>outputdecorating helloprint("calling function...")
23print("calling function...")24hello()outputcalling function...def wrapper():
7def wrapper():8 print(f"calling {func.__name__hello}")9 func()outputcalling hellodef hello():
8 print(f"calling {func.__name__}")9 func()1011 return wrapper121314# Decorator runs at definition time15print("defining function...")161718@trace19def hello():20 print("hello from function")outputhello from functionhello()
23print("calling function...")24hello()
Metadata Problem
metadata.py
Replay: real traced execution (multi-file project)
# Preserving metadata
from functools import wraps
def without_wraps(func):
def wrapper():
func()
return wrapper
def with_wraps(func):
@wraps(func)
def wrapper():
func()
return wrapper
@without_wraps
def func_a():
"""doc for func_a"""
pass
@with_wraps
def func_b():
"""doc for func_b"""
pass
# Compare metadata
print("without wraps:")
print(" name:", func_a.__name__)
print(" doc:", func_a.__doc__)
print("\nwith wraps:")
print(" name:", func_b.__name__)
print(" doc:", func_b.__doc__)
def without_wraps(func):
6def without_wraps(func⟨function func_a A⟩):7 def wrapper():8 func()910 return wrapper<function without_wraps.<locals>.wrapper at ⟨addr B⟩>def with_wraps(func):
13def with_wraps(func⟨function func_b C⟩):14 @wraps(func)15 def wrapper():16 func()1718 return wrapper⟨function func_b D⟩print(" name:", func_a.__name__)
33# Compare metadata34print("without wraps:")35print(" name:", func_a.__name__wrapper)36print(" doc:", func_a.__doc__None)3738print("\nwith wraps:")39print(" name:", func_b.__name__func_b)40print(" doc:", func_b.__doc__None)outputwithout wraps: name: wrapper doc: None with wraps: name: func_b doc: None
Exercise: practical.py
Create a retry decorator that attempts a function multiple times