Decorators that take arguments, and the practical ones you will write: timing, caching, access checks.
# decorator factory
def repeat(times):
def decorator(func):
def wrapper(*args):
for _ in range(times):
result = func(*args)
return result
return wrapper
return decorator
@repeat(times=3)
def greet():
print("Hello")
greet()
# ans: Hello (3 times)
@repeat(times=3) first calls repeat(3); whatever it returns is used as the decorator.
import time
# measure execution time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}: {end-start}s")
return result
return wrapper
@timer
def slow():
time.sleep(1)
Calling slow() prints about slow: 1.0s: the wrapper reads the clock before and after the real call.
from functools import wraps
# simple cache
def cache(func):
cached = {}
@wraps(func)
def wrapper(*args):
if args in cached:
return cached[args]
result = func(*args)
cached[args] = result
return result
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Green calls are answered from cached without recursing. Without the cache, fibonacci(5) makes 15 calls.
# check permission
def require_auth(func):
def wrapper(*args, **kwargs):
if not is_authenticated():
raise PermissionError()
return func(*args, **kwargs)
return wrapper
@require_auth
def sensitive_operation():
return "Secret"
The wrapper is a guard: if the check fails, the original function never runs.
Predict the answer, then reveal it.
# decorator with args?
def repeat(n):
def decorator(func):
def wrapper():
for _ in range(n):
func()
return wrapper
return decorator
@repeat(2)
def func():
print("Hi")
func()
# ans: Hi (twice)
# lru_cache?
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# ans: built-in caching
# multiple decorators with args?
@decorator1(arg1)
@decorator2(arg2)
def func():
pass
# ans: both decorators get args
# decorator counting calls?
def count_calls(func):
def wrapper(*args):
wrapper.calls += 1
return func(*args)
wrapper.calls = 0
return wrapper
@count_calls
def func():
pass
func()
func.calls
# ans: 1
# retry decorator?
def retry(max_attempts):
def decorator(func):
def wrapper(*args):
for attempt in range(max_attempts):
try:
return func(*args)
except:
if attempt == max_attempts-1:
raise
return wrapper
return decorator
Each failure is swallowed and retried; on the last attempt the bare raise lets the error through.
# validation decorator?
def validate(type_):
def decorator(func):
def wrapper(arg):
if not isinstance(arg, type_):
raise TypeError()
return func(arg)
return wrapper
return decorator
@validate(int)
def double(x):
return x * 2
double(4) returns 8; double("4") raises TypeError before double runs.
# property as decorator?
class MyClass:
@property
def value(self):
return 5
# ans: property is a decorator
# classmethod as decorator?
class MyClass:
@classmethod
def method(cls):
pass
# ans: classmethod is a decorator
# staticmethod as decorator?
class MyClass:
@staticmethod
def method():
pass
# ans: staticmethod is a decorator
# decorator preserving signature?
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# ans: preserves func signature
@d(x) calls d(x) first; it must return the actual decorator.
A dict in the enclosing scope (a cache) or an attribute on the wrapper (a call count) lives between calls.
Raise before calling the original to guard it; catch and retry to make it robust.
lru_cache, property, classmethod, staticmethod and wraps are all decorators.