Decorators
Decorators with Arguments
A simple @retry decorator is useful, but @retry(max_attempts=3) is more flexible. Decorator factories let you configure decorator behavior with parameters, turning a single decorator into a family of related behaviors.
A decorator with arguments is actually a decorator factory: a function that returns a decorator.
Factory Pattern
factory.py
Replay: real traced execution (multi-file project)
# Decorator factory pattern
from functools import wraps
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# Use decorator with argument
@repeat(times=3)
def greet(name):
print(f"hello, {name}")
return "ok"
name = "Alice"
greet(name)
# Decorator factory pattern
from functools import wraps
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# Use decorator with argument
@repeat(times=3)
def greet(name):
print(f"hello, {name}")
return "ok"
name = "Maya"
greet(name)
# Decorator factory pattern
from functools import wraps
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# Use decorator with argument
@repeat(times=3)
def greet(name):
print(f"hello, {name}")
return "ok"
name = "Jordan"
greet(name)
def repeat(times):
6def repeat(times3):7 """Decorator that repeats function execution."""89 def decorator(func):10 @wraps(func)11 def wrapper(*args, **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)14 return result1516 return wrapper1718 return decorator<function repeat.<locals>.decorator at ⟨addr A⟩>def decorator(func):
9def decorator(func⟨function greet B⟩):10 @wraps(func)11 def wrapper(*args, **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)14 return result1516 return wrapper⟨function greet C⟩name ← Alice
28name→ Alice = "Alice"29#@name="Maya", "Jordan"30greet(nameAlice)def wrapper(*args, **kwargs):
10@wraps(func)11def wrapper(*args('Alice',), **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)for _ in range(times):
pass 1 of 311def wrapper(*args, **kwargs):12 for _0 in range(times3):13 result = func(*args('Alice',), **kwargs{})14 return resultAll 3 passes — pass 1 is the card above pass _1 0 2 1 3 2 def greet(name):
pass 1 of 322@repeat(times=3)23def greet(nameAlice):24 print(f"hello, {nameAlice}")25 return "ok"outputhello, Aliceresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Alice',), **kwargs{})14return resultresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Alice',), **kwargs{})14return resultresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Alice',), **kwargs{})14return resultokgreet(name)
29#@name="Maya", "Jordan"30greet(nameAlice)
def repeat(times):
6def repeat(times3):7 """Decorator that repeats function execution."""89 def decorator(func):10 @wraps(func)11 def wrapper(*args, **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)14 return result1516 return wrapper1718 return decorator<function repeat.<locals>.decorator at ⟨addr A⟩>def decorator(func):
9def decorator(func⟨function greet B⟩):10 @wraps(func)11 def wrapper(*args, **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)14 return result1516 return wrapper⟨function greet C⟩name ← Maya
28name→ Maya = "Maya"29greet(nameMaya)def wrapper(*args, **kwargs):
10@wraps(func)11def wrapper(*args('Maya',), **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)for _ in range(times):
pass 1 of 311def wrapper(*args, **kwargs):12 for _0 in range(times3):13 result = func(*args('Maya',), **kwargs{})14 return resultAll 3 passes — pass 1 is the card above pass _1 0 2 1 3 2 def greet(name):
pass 1 of 322@repeat(times=3)23def greet(nameMaya):24 print(f"hello, {nameMaya}")25 return "ok"outputhello, Mayaresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Maya',), **kwargs{})14return resultresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Maya',), **kwargs{})14return resultresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Maya',), **kwargs{})14return resultokgreet(name)
28name = "Maya"29greet(nameMaya)
def repeat(times):
6def repeat(times3):7 """Decorator that repeats function execution."""89 def decorator(func):10 @wraps(func)11 def wrapper(*args, **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)14 return result1516 return wrapper1718 return decorator<function repeat.<locals>.decorator at ⟨addr A⟩>def decorator(func):
9def decorator(func⟨function greet B⟩):10 @wraps(func)11 def wrapper(*args, **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)14 return result1516 return wrapper⟨function greet C⟩name ← Jordan
28name→ Jordan = "Jordan"29greet(nameJordan)def wrapper(*args, **kwargs):
10@wraps(func)11def wrapper(*args('Jordan',), **kwargs):12 for _ in range(times):13 result = func(*args, **kwargs)for _ in range(times):
pass 1 of 311def wrapper(*args, **kwargs):12 for _0 in range(times3):13 result = func(*args('Jordan',), **kwargs{})14 return resultAll 3 passes — pass 1 is the card above pass _1 0 2 1 3 2 def greet(name):
pass 1 of 322@repeat(times=3)23def greet(nameJordan):24 print(f"hello, {nameJordan}")25 return "ok"outputhello, Jordanresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Jordan',), **kwargs{})14return resultresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Jordan',), **kwargs{})14return resultresult ← ok
12for _ in range(times):13 result→ ok = func(*args('Jordan',), **kwargs{})14return resultokgreet(name)
28name = "Jordan"29greet(nameJordan)
decorator factory - a function that takes parameters and returns a decorator, enabling configurable decoration
How It Works
This is equivalent to:
my_func = decorator_with_args(val1, val2)(my_func)
Parametrized Decorators
parametrized.py
Replay: real traced execution (multi-file project)
# Parametrized decorator
from functools import wraps
def log_with_prefix(prefix):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix}: calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log_with_prefix("[INFO]")
def task_a():
return "done"
@log_with_prefix("[DEBUG]")
def task_b():
return "ok"
task_a()
task_b()
def log_with_prefix(prefix):
pass 1 of 26def log_with_prefix(prefix[INFO]):7 def decorator(func):8 @wraps(func)9 def wrapper(*args, **kwargs):10 print(f"{prefix}: calling {func.__name__}")11 return func(*args, **kwargs)1213 return wrapper1415 return decorator<function log_with_prefix.<locals>.decorator at ⟨addr A⟩>def decorator(func):
pass 1 of 26def log_with_prefix(prefix):7 def decorator(func⟨function task_a B⟩):8 @wraps(func)9 def wrapper(*args, **kwargs):10 print(f"{prefix}: calling {func.__name__}")11 return func(*args, **kwargs)1213 return wrapper⟨function task_a C⟩def log_with_prefix(prefix):
pass 2 of 26def log_with_prefix(prefix[DEBUG]):7 def decorator(func):8 @wraps(func)9 def wrapper(*args, **kwargs):10 print(f"{prefix}: calling {func.__name__}")11 return func(*args, **kwargs)1213 return wrapper1415 return decorator<function log_with_prefix.<locals>.decorator at ⟨addr A⟩>def decorator(func):
pass 2 of 26def log_with_prefix(prefix):7 def decorator(func⟨function task_b D⟩):8 @wraps(func)9 def wrapper(*args, **kwargs):10 print(f"{prefix}: calling {func.__name__}")11 return func(*args, **kwargs)1213 return wrapper⟨function task_b E⟩task_a()
28task_a()29task_b()def wrapper(*args, **kwargs):
pass 1 of 28@wraps(func)9def wrapper(*args(), **kwargs):10 print(f"{prefix[INFO]}: calling {func.__name__task_a}")11 return func(*args(), **kwargs{})output[INFO]: calling task_atask_a()
28task_a()29task_b()def wrapper(*args, **kwargs):
pass 2 of 28@wraps(func)9def wrapper(*args(), **kwargs):10 print(f"{prefix[DEBUG]}: calling {func.__name__task_b}")11 return func(*args(), **kwargs{})output[DEBUG]: calling task_btask_b()
28task_a()29task_b()
Multiple Parameters
multiple_params.py
Replay: real traced execution (multi-file project)
# Multiple parameters
from functools import wraps
def throttle(max_calls, period_seconds):
"""Allow max_calls within period_seconds."""
def decorator(func):
calls = []
@wraps(func)
def wrapper(*args, **kwargs):
now = 1000.0
# Remove old calls
while calls and calls[0] < now - period_seconds:
calls.pop(0)
if len(calls) >= max_calls:
raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@throttle(max_calls=2, period_seconds=1)
def api_call():
print("API called")
api_call()
api_call()
# Third call would raise if done quickly
def throttle(max_calls, period_seconds):
6def throttle(max_calls2, period_seconds1):7 """Allow max_calls within period_seconds."""89 def decorator(func):10 calls = []1112 @wraps(func)13 def wrapper(*args, **kwargs):14 now = 1000.015 # Remove old calls16 while calls and calls[0] < now - period_seconds:17 calls.pop(0)1819 if len(calls) >= max_calls:20 raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122 calls.append(now)23 return func(*args, **kwargs)2425 return wrapper2627 return decorator<function throttle.<locals>.decorator at ⟨addr A⟩>calls ← []
9def decorator(func⟨function api_call B⟩):10 calls→ [] = []1112 @wraps(func)13 def wrapper(*args, **kwargs):14 now = 1000.015 # Remove old calls16 while calls and calls[0] < now - period_seconds:17 calls.pop(0)1819 if len(calls) >= max_calls:20 raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122 calls.append(now)23 return func(*args, **kwargs)2425 return wrapper⟨function api_call C⟩api_call()
35api_call()36api_call()now ← 1000.0, calls ← [1000.0]
pass 1 of 212@wraps(func)13def wrapper(*args(), **kwargs):14 now→ 1000.0 = 1000.015 # Remove old calls16 while calls and calls[0] < now - period_seconds:17 calls.pop(0)1819 if len(calls) >= max_calls:20 raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122 calls→ [1000.0].append(now1000.0)23 return func(*args(), **kwargs{})def api_call():
pass 1 of 230@throttle(max_calls=2, period_seconds=1)31def api_call():32 print("API called")outputAPI calledapi_call()
35api_call()36api_call()37# Third call would raise if done quicklynow ← 1000.0, calls ← [1000.0, 1000.0]
pass 2 of 212@wraps(func)13def wrapper(*args(), **kwargs):14 now→ 1000.0 = 1000.015 # Remove old calls16 while calls and calls[0] < now - period_seconds:17 calls.pop(0)1819 if len(calls) >= max_calls:20 raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122 calls→ [1000.0, 1000.0].append(now1000.0)23 return func(*args(), **kwargs{})def api_call():
pass 2 of 230@throttle(max_calls=2, period_seconds=1)31def api_call():32 print("API called")outputAPI calledapi_call()
35api_call()36api_call()37# Third call would raise if done quickly
Optional Arguments
optional_args.py
Replay: real traced execution (multi-file project)
# Optional decorator arguments
from functools import wraps
def optional_prefix(arg=None):
"""Decorator that can be used with or without arguments."""
def decorator(func):
prefix = arg if arg is not None else "[LOG]"
@wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix}: {func.__name__}")
return func(*args, **kwargs)
return wrapper
# If called without parens, arg is the function itself
if callable(arg):
func = arg
arg = None
return decorator(func)
return decorator
# With argument
@optional_prefix("[WARN]")
def task_a():
pass
# Without argument
@optional_prefix
def task_b():
pass
task_a()
task_b()
def optional_prefix(arg=None):
pass 1 of 26def optional_prefix(arg[WARN]=NoneNone):7 """Decorator that can be used with or without arguments."""89 def decorator(func):10 prefix = arg if arg is not None else "[LOG]"1112 @wraps(func)13 def wrapper(*args, **kwargs):14 print(f"{prefix}: {func.__name__}")15 return func(*args, **kwargs)1617 return wrapper1819 # If called without parens, arg is the function itself20 if callable(arg):21 func = arg22 arg = None23 return decorator(func)2425 return decorator<function optional_prefix.<locals>.decorator at ⟨addr A⟩>prefix ← [WARN]
pass 1 of 29def decorator(func⟨function task_a B⟩):10 prefix→ [WARN] = arg[WARN] if arg is not None else "[LOG]"1112 @wraps(func)13 def wrapper(*args, **kwargs):14 print(f"{prefix}: {func.__name__}")15 return func(*args, **kwargs)1617 return wrapper⟨function task_a C⟩def optional_prefix(arg=None):
pass 2 of 26def optional_prefix(arg⟨function task_b A⟩=NoneNone):7 """Decorator that can be used with or without arguments."""func ← ⟨function task_b A⟩, arg ← None
19# If called without parens, arg is the function itself20if callable(arg⟨function task_b A⟩):21 func→ ⟨function task_b A⟩ = arg⟨function task_b A⟩22 arg→ None = None23 return decorator(func⟨function task_b A⟩)prefix ← [LOG]
pass 2 of 29def decorator(func⟨function task_b A⟩):10 prefix→ [LOG] = argNone if arg is not None else "[LOG]"1112 @wraps(func)13 def wrapper(*args, **kwargs):14 print(f"{prefix}: {func.__name__}")15 return func(*args, **kwargs)1617 return wrapper⟨function task_b D⟩task_a()
40task_a()41task_b()def wrapper(*args, **kwargs):
pass 1 of 212@wraps(func)13def wrapper(*args(), **kwargs):14 print(f"{prefix[WARN]}: {func.__name__task_a}")15 return func(*args(), **kwargs{})output[WARN]: task_atask_a()
40task_a()41task_b()def wrapper(*args, **kwargs):
pass 2 of 212@wraps(func)13def wrapper(*args(), **kwargs):14 print(f"{prefix[LOG]}: {func.__name__task_b}")15 return func(*args(), **kwargs{})output[LOG]: task_btask_b()
40task_a()41task_b()
optional decorator args - making decorator parameters optional with defaults
Class-Based Approach
An alternative using classes:
class_based.py
Replay: real traced execution (multi-file project)
# Class-based decorator with args
from functools import wraps
class CountCalls:
def __init__(self, max_calls):
self.max_calls = max_calls
def __call__(self, func):
count = {"value": 0}
@wraps(func)
def wrapper(*args, **kwargs):
count["value"] += 1
if count["value"] > self.max_calls:
raise RuntimeError(f"exceeded max calls: {self.max_calls}")
print(f"call {count['value']}/{self.max_calls}")
return func(*args, **kwargs)
return wrapper
@CountCalls(max_calls=3)
def task():
return "ok"
task()
task()
task()
self.max_calls ← 3
6class CountCalls:7 def __init__(self⟨CountCalls A⟩, max_calls3):8 self.max_calls→ 3 = max_calls3count ← {'value': 0}
10def __call__(self⟨CountCalls A⟩, func⟨function task B⟩):11 count→ {'value': 0} = {"value": 0}1213 @wraps(func)14 def wrapper(*args, **kwargs):15 count["value"] += 116 if count["value"] > self.max_calls:17 raise RuntimeError(f"exceeded max calls: {self.max_calls}")18 print(f"call {count['value']}/{self.max_calls}")19 return func(*args, **kwargs)2021 return wrapper⟨function task C⟩task()
29task()30task()count[”value”] ← 1
pass 1 of 313@wraps(func)14def wrapper(*args(), **kwargs):15 count["value"]→ 1 += 116 if count["value"] > self.max_calls:17 raise RuntimeError(f"exceeded max calls: {self.max_calls}")18 print(f"call {count['value']1}/{self.max_calls3}")19 return func(*args(), **kwargs{})outputcall 1/3All 3 passes — pass 1 is the card above pass count[’value’]count[”value”]1 1 0 → 1 2 2 1 → 2 3 3 2 → 3 task()
29task()30task()31task()task()
29task()30task()31task()task()
30task()31task()
Common Use Cases
- Repeat
ntimes - Retry up to
max_attempts - Cache with
max_size - Throttle to
max_calls_per_second - Validate with custom rules
Exercise: practical.py
Build a rate-limiting decorator with configurable limits