Difficulty: Advanced
Explain Python decorators. How do they work under the hood? Write a custom decorator.
A decorator is a function that takes another function and extends its behavior without modifying it. Under the hood, @decorator is syntactic sugar for: function = decorator(function).
Decorators use closures - the inner wrapper function remembers and has access to the original function.
Common use cases: logging, timing, authentication, caching, rate limiting, retry logic.
import functools
import time
def timer(func):
@functools.wraps(func) # preserves func name & docstring
def wrapper(*args, kwargs):
start = time.perf_counter()
result = func(*args, kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function(n):
"""Simulates a slow operation."""
time.sleep(n)
return f"Done after {n}s"
# @timer is equivalent to:
# slow_function = timer(slow_function)
result = slow_function(1)
print(result)
print(slow_function.__name__) # 'slow_function' (thanks to @wraps)
The decorator wraps the original function. @functools.wraps preserves the original function's metadata (name, docstring).
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
import random
if random.random() < 0.7:
raise ConnectionError("Server unavailable")
return {"data": "success"}
# Three levels of nesting:
# retry(3, 0.5) returns decorator
# decorator(fetch_data) returns wrapper
# wrapper() is called when fetch_data() is called
A decorator factory is a function that returns a decorator. This pattern allows passing configuration parameters to decorators.
def bold(func):
@functools.wraps(func)
def wrapper(*args, kwargs):
return f"<b>{func(*args, kwargs)}</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, kwargs):
return f"<i>{func(*args, kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice"))
# Equivalent to: bold(italic(greet))("Alice")
# Inner decorator applies first, outer wraps around it
Decorators stack bottom-up: @italic applies first, then @bold wraps the result. Read them inside-out.
Decorators, Closures, functools.wraps, Class Decorators, Decorator Factory