04.4 - Decorators
What is a Decorator?
A decorator is a function that wraps another function to add behavior without modifying the original code.
@decorator
def my_function():
...
# Equivalent to:
def my_function():
...
my_function = decorator(my_function)
Decorators implement the "Open/Closed Principle": open for extension, closed for modification.
Building a Decorator Step by Step
# Step 1 — A simple wrapper
def my_decorator(func):
def wrapper():
print("Before the function")
func()
print("After the function")
return wrapper
# Without @syntax
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
say_hello()
# Before the function
# Hello!
# After the function
Using @functools.wraps
Always use @functools.wraps to preserve the original function's metadata:
import functools
def my_decorator(func):
@functools.wraps(func) # preserves __name__, __doc__, etc.
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done: {func.__name__}")
return result
return wrapper
@my_decorator
def add(a, b):
"""Add two numbers."""
return a + b
add(3, 4)
# Calling add
# Done: add
# 7
add.__name__ # "add" (not "wrapper" — thanks to @wraps)
add.__doc__ # "Add two numbers."
Practical Decorators
1. Timing Decorator
import functools
import time
def timeit(func):
@functools.wraps(func)
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
@timeit
def slow_operation(n):
return sum(range(n))
slow_operation(10_000_000)
# slow_operation took 0.2341s
2. Retry Decorator
import functools
import time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_error = e
print(f"Attempt {attempt}/{times} failed: {e}")
if attempt < times:
time.sleep(delay)
raise last_error
return wrapper
return decorator
@retry(times=3, delay=0.5, exceptions=(ConnectionError,))
def fetch_data(url):
# ...
pass
3. Caching with functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(50) # instant! (cached)
# Check cache stats
fibonacci.cache_info()
# CacheInfo(hits=48, misses=51, maxsize=128, currsize=51)
# Python 3.9+ shorthand
from functools import cache # unlimited cache
@cache
def expensive(n):
...
4. Validation Decorator
def require_positive(*arg_names):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name in arg_names:
if name in bound.arguments and bound.arguments[name] <= 0:
raise ValueError(f"'{name}' must be positive, got {bound.arguments[name]}")
return func(*args, **kwargs)
return wrapper
return decorator
@require_positive("width", "height")
def area(width, height):
return width * height
area(5, 10) # 50
area(-1, 10) # ValueError: 'width' must be positive
Decorator with Arguments (Decorator Factory)
def repeat(n):
"""Run the function n times."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
Stacking Decorators
@timeit
@retry(times=3)
@lru_cache(maxsize=64)
def fetch_and_process(url):
...
# Execution order: timeit wraps retry wraps lru_cache wraps fetch_and_process
# Applied bottom-up, executed top-down
Class-Based Decorators
class Singleton:
"""Ensure only one instance of a class is created."""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in Singleton._instances:
Singleton._instances[cls] = super().__call__(*args, **kwargs)
return Singleton._instances[cls]
@Singleton
class DatabaseConnection:
def __init__(self):
self.connected = False
Key Vocabulary
| Term | Definition |
|---|---|
| Decorator | A function that wraps another function to extend behavior |
@syntax | Syntactic sugar: @dec before def f means f = dec(f) |
@functools.wraps | Preserves the wrapped function's name, docstring, etc. |
| Wrapper | The inner function inside a decorator |
@lru_cache | Memoization decorator from functools |
| Decorator factory | A function that returns a decorator (for parameterized decorators) |
| Stacking decorators | Applying multiple decorators to one function |
| Cross-cutting concern | Logic applied to many functions (logging, timing, auth) |
Summary
- A decorator is a function that takes a function and returns a modified version
- Always use
@functools.wraps(func)inside decorators to preserve metadata - Common built-in decorators:
@lru_cache,@cache,@staticmethod,@classmethod,@property - Decorators with arguments use a factory pattern: function → decorator → wrapper
- Stack multiple decorators — they apply bottom-up, execute top-down
- Use decorators for cross-cutting concerns: logging, timing, retry, caching, validation
📄️ 04.1 - Function Basics
Define reusable, documented functions with parameters, return values, default arguments, and type hints
📄️ 04.2 - *args & **kwargs
Write flexible functions with variable arguments, keyword arguments, and capture state with closures
📄️ 04.3 - Lambda & Functional
Write concise anonymous functions and apply functional programming patterns with Python's built-in tools
📄️ 04.4 - Decorators
Understand and write Python decorators to add cross-cutting concerns like logging, timing, caching, and validation
📄️ Lab - Module 04
Build a data processing pipeline using functions, lambdas, map/filter/reduce, closures, and decorators
📄️ Quiz - Module 04
30 questions on function basics, *args/**kwargs, lambda, map/filter/reduce, closures, and decorators