Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on multiple decorators! ๐ In this guide, weโll explore how to chain decorators in Python to create powerful, composable functions.
Youโll discover how combining decorators can transform your Python development experience. Whether youโre building web applications ๐, creating APIs ๐ฅ๏ธ, or developing automation tools ๐, understanding decorator chaining is essential for writing elegant, maintainable code.
By the end of this tutorial, youโll feel confident using multiple decorators in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Multiple Decorators
๐ค What is Decorator Chaining?
Decorator chaining is like adding layers to a gift ๐. Think of it as wrapping a present multiple times - each wrapper adds its own special touch to the final package!
In Python terms, multiple decorators allow you to apply several modifications to a function, with each decorator adding its own functionality. This means you can:
- โจ Add multiple behaviors without modifying the original function
- ๐ Create reusable, composable function enhancements
- ๐ก๏ธ Keep your code clean and organized
๐ก Why Use Multiple Decorators?
Hereโs why developers love decorator chaining:
- Separation of Concerns ๐: Each decorator handles one specific task
- Reusability ๐ป: Mix and match decorators as needed
- Clean Code ๐: Avoid complex nested logic
- Flexibility ๐ง: Add or remove features easily
Real-world example: Imagine building a web API ๐. With multiple decorators, you can add authentication, logging, and rate limiting to your endpoints with just a few lines!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, Multiple Decorators!
def bold(func):
"""Makes text bold ๐ช"""
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"**{result}**"
return wrapper
def italic(func):
"""Makes text italic ๐จ"""
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"*{result}*"
return wrapper
# ๐ฏ Using multiple decorators
@bold
@italic
def greet(name):
return f"Hello, {name}! ๐"
# Let's test it!
print(greet("Python")) # Output: ***Hello, Python! ๐***
๐ก Explanation: Notice how decorators are applied from bottom to top! First italic
wraps the function, then bold
wraps the italic version.
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Timing and logging
import time
import functools
def timer(func):
"""โฑ๏ธ Times function execution"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"โฑ๏ธ {func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
def logger(func):
"""๐ Logs function calls"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"๐ Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"โ
{func.__name__} returned: {result}")
return result
return wrapper
# ๐จ Pattern 2: Authentication and validation
def authenticate(func):
"""๐ Check if user is authenticated"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Simulating authentication check
if "user" in kwargs and kwargs["user"]:
print(f"๐ User {kwargs['user']} authenticated!")
return func(*args, **kwargs)
else:
return "โ Authentication required!"
return wrapper
def validate_positive(func):
"""โ๏ธ Validate positive numbers"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
if all(arg > 0 for arg in args if isinstance(arg, (int, float))):
return func(*args, **kwargs)
else:
return "โ All numbers must be positive!"
return wrapper
๐ก Practical Examples
๐ Example 1: E-commerce Order Processing
Letโs build something real:
# ๐๏ธ E-commerce order processing with multiple decorators
import json
from datetime import datetime
def log_order(func):
"""๐ Log order details"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
order_id = kwargs.get('order_id', 'Unknown')
print(f"๐ Processing order {order_id} at {datetime.now()}")
result = func(*args, **kwargs)
print(f"โ
Order {order_id} processed successfully!")
return result
return wrapper
def validate_payment(func):
"""๐ณ Validate payment method"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
payment = kwargs.get('payment_method', '')
valid_methods = ['credit_card', 'debit_card', 'paypal']
if payment in valid_methods:
print(f"๐ณ Payment method '{payment}' validated!")
return func(*args, **kwargs)
else:
return {"status": "error", "message": "โ Invalid payment method!"}
return wrapper
def apply_discount(func):
"""๐ Apply discount if eligible"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
total = kwargs.get('total', 0)
if total > 100:
discount = total * 0.1
kwargs['total'] = total - discount
print(f"๐ Applied 10% discount! Saved ${discount:.2f}")
return func(*args, **kwargs)
return wrapper
# ๐ Using all decorators together!
@log_order
@validate_payment
@apply_discount
def process_order(**order_details):
"""Process an e-commerce order"""
return {
"status": "success",
"order_id": order_details['order_id'],
"total": order_details['total'],
"message": "๐ Order placed successfully!"
}
# ๐ฎ Let's use it!
order = process_order(
order_id="ORD-12345",
payment_method="credit_card",
total=150.00,
items=["๐ฑ Phone Case", "๐ง Headphones"]
)
print(json.dumps(order, indent=2))
๐ฏ Try it yourself: Add a check_inventory
decorator that verifies items are in stock!
๐ฎ Example 2: Game Character Abilities
Letโs make it fun:
# ๐ Game character with stackable abilities
class GameCharacter:
def __init__(self, name, health=100):
self.name = name
self.health = health
self.mana = 50
self.damage = 10
def __str__(self):
return f"๐ฎ {self.name} | โค๏ธ {self.health} | ๐ {self.mana} | โ๏ธ {self.damage}"
def requires_mana(cost):
"""๐ Check if character has enough mana"""
def decorator(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
if self.mana >= cost:
self.mana -= cost
print(f"๐ Used {cost} mana!")
return func(self, *args, **kwargs)
else:
print(f"โ Not enough mana! Need {cost}, have {self.mana}")
return None
return wrapper
return decorator
def cooldown(seconds):
"""โฐ Add cooldown to ability"""
def decorator(func):
func._last_used = 0
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
current_time = time.time()
if current_time - func._last_used >= seconds:
func._last_used = current_time
return func(self, *args, **kwargs)
else:
remaining = seconds - (current_time - func._last_used)
print(f"โฐ Ability on cooldown! Wait {remaining:.1f} seconds")
return None
return wrapper
return decorator
def boost_damage(multiplier):
"""โ๏ธ Boost damage for special attacks"""
def decorator(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
original_damage = self.damage
self.damage *= multiplier
print(f"โ๏ธ Damage boosted by {multiplier}x!")
result = func(self, *args, **kwargs)
self.damage = original_damage
return result
return wrapper
return decorator
# ๐ฏ Create a character with abilities
class Wizard(GameCharacter):
@requires_mana(20)
@cooldown(3)
@boost_damage(2)
def fireball(self, target):
"""๐ฅ Cast a powerful fireball!"""
print(f"๐ฅ {self.name} casts Fireball at {target}!")
print(f"๐ฅ Dealt {self.damage} damage!")
return self.damage
@requires_mana(10)
@cooldown(1)
def heal(self):
"""๐ Heal yourself"""
heal_amount = 25
self.health += heal_amount
print(f"๐ {self.name} healed for {heal_amount} HP!")
return heal_amount
# ๐ฎ Let's play!
wizard = Wizard("Gandalf")
print(wizard)
# Cast some spells!
wizard.fireball("Dragon ๐")
wizard.heal()
wizard.fireball("Goblin ๐บ") # This will be on cooldown!
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Decorator Factory with Parameters
When youโre ready to level up, try this advanced pattern:
# ๐ฏ Advanced decorator factory
def rate_limit(max_calls, period):
"""๐ฆ Rate limit function calls"""
def decorator(func):
calls = []
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Remove old calls outside the period
calls[:] = [call_time for call_time in calls if now - call_time < period]
if len(calls) >= max_calls:
print(f"๐ฆ Rate limit exceeded! Max {max_calls} calls per {period}s")
return None
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
def cache_result(expiry_seconds):
"""๐พ Cache function results"""
def decorator(func):
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
result, timestamp = cache[key]
if time.time() - timestamp < expiry_seconds:
print(f"๐พ Cache hit! Returning cached result")
return result
result = func(*args, **kwargs)
cache[key] = (result, time.time())
return result
return wrapper
return decorator
# ๐ช Using advanced decorators
@rate_limit(max_calls=3, period=10)
@cache_result(expiry_seconds=5)
def expensive_api_call(endpoint):
"""Simulate an API call"""
print(f"๐ Making API call to {endpoint}...")
time.sleep(1) # Simulate network delay
return f"Response from {endpoint}"
๐๏ธ Advanced Topic 2: Class Decorators
For the brave developers:
# ๐ Combining function and class decorators
def add_repr(cls):
"""๐จ Add a nice __repr__ to any class"""
def __repr__(self):
attrs = ', '.join(f"{k}={v}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs}) โจ"
cls.__repr__ = __repr__
return cls
def track_instances(cls):
"""๐ Track all instances of a class"""
cls._instances = []
original_init = cls.__init__
def new_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
cls._instances.append(self)
cls.__init__ = new_init
cls.get_instance_count = classmethod(lambda cls: len(cls._instances))
return cls
# ๐ฏ Apply multiple decorators to a class
@add_repr
@track_instances
class Player:
def __init__(self, name, score=0):
self.name = name
self.score = score
self.achievements = []
@timer
@logger
def add_achievement(self, achievement):
"""๐ Add an achievement"""
self.achievements.append(achievement)
self.score += 10
return f"๐ Unlocked: {achievement}"
# ๐ฎ Test it out!
player1 = Player("Alice", 100)
player2 = Player("Bob", 200)
print(player1) # Nice repr from decorator!
print(f"๐ Total players: {Player.get_instance_count()}")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Wrong Decorator Order
# โ Wrong way - order matters!
@validate_positive # This runs second
@logger # This runs first
def calculate(x, y):
return x + y
# If logger runs first, it might log invalid inputs!
# โ
Correct way - validate first, then log
@logger # This runs second
@validate_positive # This runs first
def calculate(x, y):
return x + y
๐คฏ Pitfall 2: Forgetting functools.wraps
# โ Dangerous - loses function metadata!
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # Lost __name__, __doc__, etc!
# โ
Safe - preserves metadata!
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # Keeps all metadata! โจ
๐ ๏ธ Best Practices
- ๐ฏ Order Matters: Apply decorators from bottom to top thoughtfully
- ๐ Use functools.wraps: Always preserve function metadata
- ๐ก๏ธ Keep It Simple: Each decorator should do one thing well
- ๐จ Name Clearly:
@authenticate
not@auth
- โจ Document Behavior: Explain what each decorator does
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Web API Endpoint System
Create a decorator system for web API endpoints:
๐ Requirements:
- โ Authentication decorator (check API key)
- ๐ท๏ธ Validation decorator (validate request data)
- ๐ Analytics decorator (track API usage)
- ๐ฆ Rate limiting decorator
- ๐จ Each endpoint needs proper error handling!
๐ Bonus Points:
- Add response caching
- Implement role-based permissions
- Create decorator for API versioning
๐ก Solution
๐ Click to see solution
# ๐ฏ Our decorator-based API system!
import json
from functools import wraps
from datetime import datetime
# ๐ Authentication decorator
def require_api_key(func):
@wraps(func)
def wrapper(*args, **kwargs):
api_key = kwargs.get('headers', {}).get('api_key')
if api_key == "secret-key-123":
print("๐ API key validated!")
return func(*args, **kwargs)
else:
return {"error": "โ Invalid API key!", "status": 401}
return wrapper
# โ
Validation decorator
def validate_request(required_fields):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
data = kwargs.get('data', {})
missing = [field for field in required_fields if field not in data]
if missing:
return {"error": f"โ Missing fields: {missing}", "status": 400}
print(f"โ
Request validated! Has all fields: {required_fields}")
return func(*args, **kwargs)
return wrapper
return decorator
# ๐ Analytics decorator
def track_usage(func):
if not hasattr(track_usage, 'calls'):
track_usage.calls = {}
@wraps(func)
def wrapper(*args, **kwargs):
endpoint = func.__name__
track_usage.calls[endpoint] = track_usage.calls.get(endpoint, 0) + 1
print(f"๐ {endpoint} called {track_usage.calls[endpoint]} times")
result = func(*args, **kwargs)
# Add analytics to response
if isinstance(result, dict):
result['_analytics'] = {
'endpoint': endpoint,
'timestamp': datetime.now().isoformat(),
'call_count': track_usage.calls[endpoint]
}
return result
return wrapper
# ๐ฆ Rate limiting
def rate_limit_api(max_calls=10, window=60):
def decorator(func):
calls = []
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Clean old calls
calls[:] = [t for t in calls if now - t < window]
if len(calls) >= max_calls:
return {
"error": f"๐ฆ Rate limit exceeded! Max {max_calls} calls per {window}s",
"status": 429
}
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
# ๐ฏ Example API endpoints
class UserAPI:
@require_api_key
@rate_limit_api(max_calls=5, window=60)
@track_usage
@validate_request(['username', 'email'])
def create_user(self, **request):
"""Create a new user ๐ค"""
data = request['data']
return {
"status": 201,
"message": f"โ
User {data['username']} created!",
"user_id": f"usr_{int(time.time())}",
"emoji": "๐"
}
@require_api_key
@track_usage
def get_user(self, user_id, **request):
"""Get user details ๐"""
return {
"status": 200,
"user_id": user_id,
"username": "awesome_user",
"emoji": "๐"
}
# ๐ฎ Test our API!
api = UserAPI()
# Valid request
response = api.create_user(
headers={'api_key': 'secret-key-123'},
data={'username': 'pythonista', 'email': '[email protected]'}
)
print(json.dumps(response, indent=2))
# Get usage stats
print(f"\n๐ API Usage Stats: {track_usage.calls}")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Chain multiple decorators with confidence ๐ช
- โ Understand decorator execution order and avoid common mistakes ๐ก๏ธ
- โ Create powerful decorator combinations for real projects ๐ฏ
- โ Debug decorator issues like a pro ๐
- โ Build awesome decorator-based systems with Python! ๐
Remember: Decorators are like LEGO blocks - you can combine them in countless ways to build amazing things! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered multiple decorators!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a decorator library for your projects
- ๐ Explore class decorators and metaclasses
- ๐ Share your creative decorator combinations!
Remember: Every Python expert started with simple decorators. Keep experimenting, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ