Python Decorators — Function Enhancement Patterns
Decorators modify or enhance functions without changing their code. They are used extensively in frameworks, testing, logging, and caching.
Learning Objectives
- Understand how decorators work under the hood
- Create decorators with and without arguments
- Apply decorators to classes and methods
- Use common decorator patterns in production
Basic Decorator
import time
def timer(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
@timer
def slow_function():
time.sleep(1)
return "done"
slow_function() # slow_function took 1.0012s
Decorator with Arguments
def retry(max_attempts=3, delay=1):
def decorator(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. 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("Network error")
return {"status": "ok"}
Preserving Function Metadata
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # "greet" (not "wrapper")
print(greet.__doc__) # "Greet someone."
Class Decorators
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
self.connection = "connected"
db1 = Database()
db2 = Database()
print(db1 is db2) # True
Stacking 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")) # <b><i>Hello, Alice</i></b>
Common Production Decorators
# Caching (built-in since Python 3.8)
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Deprecation warning
import warnings
def deprecated(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(f"{func.__name__} is deprecated", DeprecationWarning)
return func(*args, **kwargs)
return wrapper
# Type checking decorator
def type_check(*types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args):
for arg, expected in zip(args, types):
if not isinstance(arg, expected):
raise TypeError(f"Expected {expected}, got {type(arg)}")
return func(*args)
return wrapper
return decorator
@type_check(int, int)
def add(a, b):
return a + b
Key Takeaways
- Decorators wrap functions to add behavior
- Always use
@functools.wrapsto preserve metadata - Decorator factories use nested functions for arguments
@lru_cacheis built-in caching- Common patterns: retry, timer, type check, deprecation, singleton