Introduction
Python decorators are powerful tools that allow developers to modify or enhance functions without changing their core implementation. This tutorial explores the advanced technique of stacking multiple decorators, demonstrating how to layer different decorators to create more complex and flexible function behaviors in Python programming.
Decorator Basics
What is a Decorator?
In Python, a decorator is a powerful and flexible way to modify or enhance functions or classes without directly changing their source code. Essentially, decorators are functions that take another function as an argument and return a modified version of that function.
Basic Decorator Syntax
def my_decorator(func):
def wrapper():
print("Something before the function is called.")
func()
print("Something after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Key Characteristics of Decorators
Function Wrapping
Decorators wrap around existing functions, adding extra functionality before or after the original function execution.
graph LR
A[Original Function] --> B[Decorator Wrapper]
B --> C[Enhanced Functionality]
Types of Decorators
| Decorator Type | Description | Example Use Case |
|---|---|---|
| Function Decorators | Modify function behavior | Logging, timing |
| Class Decorators | Modify class behavior | Singleton pattern |
| Method Decorators | Modify method behavior | Access control |
Practical Example: Timing Decorator
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function {func.__name__} took {end_time - start_time} seconds to execute.")
return result
return wrapper
@timer_decorator
def slow_function():
time.sleep(2)
print("Slow function completed")
slow_function()
Decorator with Arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("LabEx User")
Common Use Cases
- Logging
- Performance measurement
- Authentication
- Caching
- Input validation
By understanding these basics, you'll be well-prepared to explore more advanced decorator techniques in Python. LabEx recommends practicing these concepts to gain proficiency.
Chaining Decorators
Understanding Decorator Chaining
Decorator chaining allows multiple decorators to be applied to a single function. When multiple decorators are used, they are applied from bottom to top.
graph TD
A[Original Function] --> B[Decorator 1]
B --> C[Decorator 2]
C --> D[Decorator 3]
D --> E[Final Enhanced Function]
Basic Decorator Chaining Example
def decorator1(func):
def wrapper(*args, **kwargs):
print("Decorator 1 - Before function")
result = func(*args, **kwargs)
print("Decorator 1 - After function")
return result
return wrapper
def decorator2(func):
def wrapper(*args, **kwargs):
print("Decorator 2 - Before function")
result = func(*args, **kwargs)
print("Decorator 2 - After function")
return result
return wrapper
@decorator1
@decorator2
def example_function():
print("Inside the original function")
example_function()
Practical Chaining Scenarios
Logging and Timing Decorator
import time
import functools
def log_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
return func(*args, **kwargs)
return wrapper
def timer_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function took {end_time - start_time:.4f} seconds")
return result
return wrapper
@log_decorator
@timer_decorator
def complex_calculation(n):
return sum(i**2 for i in range(n))
complex_calculation(10000)
Decorator Chaining Considerations
| Consideration | Description | Impact |
|---|---|---|
| Order Matters | Decorators apply from bottom to top | Changes function behavior |
| Performance | Multiple decorators can add overhead | Consider complexity |
| Readability | Too many decorators can reduce code clarity | Use judiciously |
Advanced Chaining with Parameters
def validate_input(min_value=0, max_value=100):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if not (min_value <= arg <= max_value):
raise ValueError(f"Input must be between {min_value} and {max_value}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_input(min_value=10, max_value=50)
@log_decorator
def process_number(x):
return x * 2
process_number(30) ## Works
## process_number(5) ## Raises ValueError
Best Practices
- Use
functools.wrapsto preserve function metadata - Keep decorators focused and single-purpose
- Be mindful of performance implications
- Document complex decorator chains
LabEx recommends practicing decorator chaining to master this powerful Python technique.
Practical Use Cases
Authentication and Authorization
def require_auth(role):
def decorator(func):
def wrapper(*args, **kwargs):
## Simulated authentication check
current_user = {
'username': 'admin',
'role': 'admin'
}
if current_user['role'] == role:
return func(*args, **kwargs)
else:
raise PermissionError("Unauthorized access")
return wrapper
return decorator
@require_auth('admin')
def delete_user(user_id):
print(f"Deleting user {user_id}")
delete_user(123) ## Works for admin
Caching Mechanism
def memoize(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100)) ## Efficient computation
Performance Monitoring
import time
import functools
def performance_tracker(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
@performance_tracker
def heavy_computation(n):
return sum(i**2 for i in range(n))
heavy_computation(1000000)
Rate Limiting
from functools import wraps
import time
def rate_limit(max_calls, time_frame):
calls = []
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
calls[:] = [c for c in calls if now - c < time_frame]
if len(calls) >= max_calls:
raise Exception("Rate limit exceeded")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, time_frame=60)
def api_call():
print("API called successfully")
## Demonstrates rate limiting
for _ in range(4):
try:
api_call()
except Exception as e:
print(e)
Logging and Debugging
import logging
import functools
def log_calls(logger=None):
if logger is None:
logger = logging.getLogger(__name__)
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.info(f"Calling {func.__name__}")
try:
result = func(*args, **kwargs)
logger.info(f"{func.__name__} completed successfully")
return result
except Exception as e:
logger.error(f"Error in {func.__name__}: {e}")
raise
return wrapper
return decorator
@log_calls()
def divide(a, b):
return a / b
divide(10, 2)
## divide(10, 0) ## Would log an error
Use Case Comparison
| Use Case | Primary Purpose | Key Benefits |
|---|---|---|
| Authentication | Control access | Security |
| Caching | Optimize performance | Reduced computation |
| Monitoring | Track performance | Insights |
| Rate Limiting | Prevent abuse | Resource protection |
| Logging | Track function calls | Debugging |
Advanced Decorator Patterns
graph TD
A[Decorator Patterns] --> B[Functional Decorators]
A --> C[Class Decorators]
A --> D[Parametrized Decorators]
A --> E[Chained Decorators]
Best Practices
- Keep decorators focused
- Use
functools.wrapsto preserve metadata - Handle exceptions gracefully
- Consider performance implications
LabEx recommends exploring these practical use cases to enhance your Python programming skills.
Summary
By understanding how to stack multiple decorators in Python, developers can create more modular, reusable, and sophisticated code. This technique enables precise function modification, allows combining different decorators with ease, and provides a clean, readable approach to extending function capabilities without compromising the original function's core logic.



