Decorators: wrap functions without rewriting them
A decorator is a callable that takes a function and returns a function. The `@decorator` syntax above a `def` is sugar for `func = decorator(func)`. The wrapped function can run pre and post logic, modify arguments, cache results, or replace the body entirely. `functools.lru_cache`, `@staticmethod`, `@property`, and Flask's `@app.route` are all decorators.
The canonical template uses an inner `wrapper` function. `def wrapper(*args, **kwargs)` accepts arbitrary arguments, calls the original `func(*args, **kwargs)`, and returns the result. Anything before or after the call runs on every invocation: log the call, time it, validate inputs, catch exceptions. Decorate `wrapper` with `@functools.wraps(func)` so the wrapped function keeps its original `__name__`, `__doc__`, and signature. Without `wraps`, introspection tools (help, pytest, Flask URL rules) see "wrapper" instead of the real function name.
Parameterized decorators add one more layer. The pattern: `decorator(arg)` returns `decorator`, which returns `wrapper`. Three nested functions instead of two. Use this when the decorator needs configuration: `@retry(times=3)`, `@route("/users", methods=["GET", "POST"])`, `@cache(ttl=60)`. The outer call captures the configuration in a closure, the middle layer captures the function, the inner wrapper runs at call time.
import functools
import time
def timed(func):
"""Print how long the call took."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = (time.perf_counter() - start) * 1000
print(f"{func.__name__} took {elapsed:.2f} ms")
return result
return wrapper
def retry(times: int):
"""Parameterized: re-call on exception, up to 'times' total attempts."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == times:
raise
print(f"attempt {attempt} failed: {e}")
return wrapper
return decorator
@timed
@retry(times=3)
def fetch(n: int) -> int:
return sum(i * i for i in range(n))
print("Result:", fetch(100_000))