Python Decorator Pattern
The core idea of the decorator pattern is wrapping β by creating a wrapper object (decorator) to wrap the original object, thereby extending its functionality without modifying the original object itself.
Basic Concepts
The decorator pattern consists of four main roles:
- Component Interface (Component): Defines the common interface for both the decorated object and the decorator.
- Concrete Component: The original object that needs to be decorated.
- Decorator Base Class (Decorator): Holds a reference to a component object and implements the component interface.
- Concrete Decorator: Implements specific decoration functionality.
Basic Syntax of Decorators
Function Decorators
Function decorators are the most common form of decorators. They accept a function as an argument and return a new function.
Example
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Some operations before function execution")
result = func(*args, **kwargs)
print("Some operations after function execution")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
# Using the decorator
say_hello("Alice")
Output:
Some operations before function execution
Hello, Alice!
Some operations after function execution
Decorators with Arguments
To pass arguments to a decorator, an additional layer of nesting is required:
Example
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(times):
print(f"Execution {i+1}:")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Bob")
Class Decorators
Besides function decorators, Python also supports class decorators. Class decorators work by implementing the __call__ method.
Basic Class Decorator
Example
class TimerDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
import time
start_time = time.time()
result = self.func(*args, **kwargs)
end_time = time.time()
print(f"Function {self.func.__name__} execution time: {end_time - start_time:.4f} seconds")
return result
@TimerDecorator
def calculate_sum(n):
return sum(range(n))
result = calculate_sum(1000000)
print(f"Calculation result: {result}")
Class Decorator with Arguments
Example
class LogDecorator:
def __init__(self, level="INFO"):
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
print(f"[{self.level}] Calling function: {func.__name__}")
print(f"[{self.level}] Arguments: args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[{self.level}] Return value: {result}")
return result
return wrapper
@LogDecorator(level="DEBUG")
def multiply(a, b):
return a * b
multiply(5, 3)
Built-in Decorators
Python provides several useful built-in decorators:
@staticmethod and @classmethod
Example
class Calculator:
@staticmethod
def add(x, y):
return x + y
@classmethod
def multiply(cls, x, y):
return x * y
# Using static method
result1 = Calculator.add(5, 3)
print(f"Static method result: {result1}")
# Using class method
result2 = Calculator.multiply(5, 3)
print(f"Class method result: {result2}")
@property
Example
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Area: {circle.area}")
circle.radius = 10
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area}")
Practical Applications of Decorators
1. Logging
Example
def log_execution(func):
def wrapper(*args, **kwargs):
print(f"Starting execution: {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"Successfully completed: {func.__name__}")
return result
except Exception as e:
print(f"Execution failed: {func.__name__}, Error: {e}")
raise
return wrapper
@log_execution
def process_data(data):
# Simulate data processing
if not data:
raise ValueError("Data cannot be empty")
return [x * 2 for x in data]
# Test
data = [1, 2, 3]
result = process_data(data)
print(f"Processing result: {result}")
2. Permission Validation
Example
def require_login(func):
def wrapper(user, *args, **kwargs):
if not user.get('is_authenticated', False):
raise PermissionError("User not logged in")
return func(user, *args, **kwargs)
return wrapper
@require_login
def view_profile(user):
return f"Viewing profile for user {user['username']}"
# Test
user1 = {'username': 'alice', 'is_authenticated': True}
user2 = {'username': 'bob', 'is_authenticated': False}
print(view_profile(user1)) # Executes normally
# print(view_profile(user2)) # Raises PermissionError
3. Caching Decorator
Example
def cache_results(func):
cache = {}
def wrapper(*args):
if args in cache:
print(f"Retrieving result from cache: {args}")
return cache
result = func(*args)
cache = result
print(f"Computing and caching result: {args} -> {result}")
return result
return wrapper
@cache_results
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Test caching effect
print(fibonacci(5))
print(fibonacci(5)) # This time, result is retrieved from cache
Execution Order of Multiple Decorators
When multiple decorators are used, their execution order is from bottom to top:
Example
def decorator1(func):
def wrapper():
print("Decorator 1 - Before")
func()
print("Decorator 1 - After")
return wrapper
def decorator2(func):
def wrapper():
print("Decorator 2 - Before")
func()
print("Decorator 2 - After")
return wrapper
@decorator1
@decorator2
def my_function():
print("Original function")
my_function()
Output:
Decorator 1 - Before
Decorator 2 - Before
Original function
Decorator 2 - After
Decorator 1 - After
Preserving Function Metadata
When using decorators, the original function's metadata (e.g., function name, docstring) gets overwritten by the wrapper function. Use functools.wraps to preserve this metadata:
Example
from functools import wraps
def preserve_metadata(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Docstring of the wrapper function"""
return func(*args, **kwargs)
return wrapper
@preserve_metadata
def example_function():
"""This is the docstring of the original function"""
pass
print(f"Function name: {example_function.__name__}")
print(f"Docstring: {example_function.__doc__}")
Practice Exercises
Exercise 1: Create a Performance Monitoring Decorator
Write a decorator to monitor function execution time and memory usage.
Example
import time
import tracemalloc
from functools import wraps
def performance_monitor(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Start memory tracking
tracemalloc.start()
# Record start time
start_time = time.time()
# Execute function
result = func(*args, **kwargs)
# Record end time
end_time = time.time()
# Get memory usage
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"Performance report for function {func.__name__}:")
print(f"Execution time: {end_time - start_time:.4f} seconds")
print(f"Memory usage: Current {current/1024:.2f} KB, Peak {peak/1024:.2f} KB")
return result
return wrapper
@performance_monitor
def process_large_data():
"""Simulate processing large data"""
data = [i**2 for i in range(100000)]
return sum(data)
process_large_data()
Exercise 2: Implement a Retry Mechanism Decorator
Create a decorator that automatically retries a function a specified number of times upon failure.
Example
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
print(f"Function {func.__name__} failed after maximum retry attempts")
raise
print(f"Function {func.__name__} failed on attempt {attempts}: {e}")
print(f"Retrying after {delay} seconds...")
time.sleep(delay)
return None
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def unstable_operation():
"""Simulate an unstable operation"""
import random
if random.random() < 0.7: # 70% chance of failure
raise ValueError("Random failure")
return "Operation successful"
# Test retry mechanism
result = unstable_operation()
print(f"Final result: {result}")
Summary
Python decorators are a powerful and flexible tool, offering the following advantages:
- Code Reusability: Separates cross-cutting concerns (e.g., logging, caching, validation) from business logic.
- Dynamic Extension: Adds new functionality without modifying original code.
- Conciseness: Uses
@syntax for clearer, more readable code. - Open/Closed Principle: Open for extension, closed for modification.
Best Practices for Using Decorators:
- Use
functools.wrapsto preserve function metadata. - Maintain single responsibility for each decorator.
- Handle exceptions appropriately within decorators.
- Be mindful of decorator execution order.
YouTip