Python Decorators
Decorators are a powerful Python feature that lets you modify or extend the behavior of functions and classes without changing their code. They wrap a function with another function.
Understanding Decorators
A decorator is a function that takes another function as input and returns a modified version:
python
# Without decorator syntax
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def greet(name):
return f"Hello, {name}"
# Manually decorating
greet = shout(greet)
print(greet("Alice")) # HELLO, ALICE
# With @ decorator syntax (same thing, cleaner)
@shout
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # HELLO, ALICECreating Decorators
Basic Decorator
python
def log_call(func):
"""Log when a function is called."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 5)
# Calling add((3, 5), {})
# add returned: 8Preserving Function Metadata
Use functools.wraps to preserve the original function's name and docstring:
python
from functools import wraps
def timer(func):
"""Time a function's execution."""
@wraps(func) # Preserves func's metadata
def wrapper(*args, **kwargs):
import time
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():
"""A deliberately slow function."""
import time
time.sleep(0.5)
return "done"
slow_function()
# slow_function took 0.5012s
# Metadata is preserved
print(slow_function.__name__) # slow_function (not 'wrapper')
print(slow_function.__doc__) # A deliberately slow function.Decorators with Arguments
python
from functools import wraps
def repeat(times):
"""Decorator that repeats a function call."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
@repeat(3)
def greet(name):
return f"Hello, {name}!"
print(greet("Alice"))
# ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']Common Decorator Patterns
Retry Decorator
python
from functools import wraps
import time
def retry(max_attempts=3, delay=1):
"""Retry a function on failure."""
def decorator(func):
@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 unstable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("Server unavailable")
return "Success!"Cache Decorator
python
from functools import wraps
def cache(func):
"""Simple caching decorator."""
memo = {}
@wraps(func)
def wrapper(*args):
if args not in memo:
memo[args] = func(*args)
return memo[args]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 354224848179261915075 (instant!)
# Python has a built-in version:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)Validation Decorator
python
from functools import wraps
def validate_types(**expected_types):
"""Validate argument types."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Check positional args
import inspect
sig = inspect.signature(func)
params = list(sig.parameters.keys())
for i, (param, value) in enumerate(zip(params, args)):
if param in expected_types:
if not isinstance(value, expected_types[param]):
raise TypeError(
f"{param} must be {expected_types[param].__name__}, "
f"got {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {"name": name, "age": age}
print(create_user("Alice", 30)) # {'name': 'Alice', 'age': 30}
# create_user("Alice", "30") # TypeError: age must be int, got strStacking Decorators
Multiple decorators can be applied to a single function:
python
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@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>
# Applied bottom-up: italic first, then bold
# Equivalent to: bold(italic(greet))Class Decorators
Decorators can also be applied to classes:
python
def singleton(cls):
"""Ensure only one instance of a class exists."""
instances = {}
@wraps(cls, updated=[])
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, url):
self.url = url
print(f"Connecting to {url}")
# Only creates one instance
db1 = Database("localhost:5432") # Connecting to localhost:5432
db2 = Database("other:5432") # No output - returns existing instance
print(db1 is db2) # TrueBuilt-in Decorators
python
class MyClass:
# @property - getter/setter
@property
def name(self):
return self._name
# @staticmethod - no self or cls
@staticmethod
def utility():
return "I don't need an instance"
# @classmethod - receives cls
@classmethod
def create(cls, data):
return cls()
# @abstractmethod - must be implemented by subclasses
from abc import abstractmethod
# @functools.lru_cache - memoization
from functools import lru_cache
@lru_cache(maxsize=None)
def expensive_computation(self, n):
return sum(range(n))Summary
- Decorators wrap functions to modify their behavior
- Use
@decoratorsyntax above the function definition - Always use
@functools.wrapsto preserve function metadata - Decorators with arguments need an extra nesting level
- Common patterns: logging, timing, caching, retry, validation
- Stack decorators by applying multiple
@(bottom-up order) - Built-in decorators:
@property,@staticmethod,@classmethod,@lru_cache
Next, we'll learn about Python file handling.