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 decorators with arguments! ๐ In this guide, weโll explore how to create powerful decorators that can accept parameters to customize their behavior.
Youโll discover how decorators with arguments can transform your Python development experience. Whether youโre building web applications ๐, implementing caching systems ๐พ, or creating validation logic ๐ก๏ธ, understanding decorators with arguments is essential for writing flexible, reusable code.
By the end of this tutorial, youโll feel confident using decorators with arguments in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Decorators with Arguments
๐ค What are Decorators with Arguments?
Decorators with arguments are like customizable gift wrappers ๐. Think of it as a wrapping service where you can specify the color, pattern, and ribbon style for each gift!
In Python terms, decorators with arguments are functions that return decorators. This means you can:
- โจ Customize decorator behavior on the fly
- ๐ Create reusable decorators with different configurations
- ๐ก๏ธ Build flexible validation and logging systems
๐ก Why Use Decorators with Arguments?
Hereโs why developers love decorators with arguments:
- Flexibility ๐: One decorator, multiple configurations
- Reusability โป๏ธ: Write once, use with different parameters
- Clean Code ๐: Keep logic separate and organized
- Dynamic Behavior โก: Adjust functionality without changing code
Real-world example: Imagine building a rate limiter ๐ฆ. With decorators with arguments, you can specify different limits for different functions!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, decorators with arguments!
def repeat(times):
"""๐ Decorator that repeats function execution"""
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for i in range(times):
print(f"๐ฏ Execution #{i + 1}")
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
# ๐จ Using the decorator with arguments
@repeat(times=3)
def greet(name):
"""๐ Say hello to someone"""
return f"Hello, {name}! ๐"
# ๐ Call the decorated function
results = greet("Python Developer")
print(f"๐ All results: {results}")
๐ก Explanation: Notice how repeat
takes an argument (times
) and returns a decorator. The decorator then wraps our function with the specified behavior!
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Validation decorator
def validate_range(min_val, max_val):
"""๐ก๏ธ Validate numeric inputs are within range"""
def decorator(func):
def wrapper(value):
if not min_val <= value <= max_val:
raise ValueError(f"โ Value must be between {min_val} and {max_val}")
return func(value)
return wrapper
return decorator
# ๐จ Pattern 2: Timing decorator with units
def timer(unit='seconds'):
"""โฑ๏ธ Measure function execution time"""
import time
def decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
if unit == 'milliseconds':
duration *= 1000
print(f"โฑ๏ธ {func.__name__} took {duration:.2f}ms")
else:
print(f"โฑ๏ธ {func.__name__} took {duration:.4f}s")
return result
return wrapper
return decorator
# ๐ Pattern 3: Retry decorator
def retry(max_attempts=3, delay=1):
"""๐ Retry function on failure"""
import time
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
print(f"โ All {max_attempts} attempts failed!")
raise
print(f"โ ๏ธ Attempt {attempt + 1} failed: {e}")
print(f"๐ Retrying in {delay} seconds...")
time.sleep(delay)
return wrapper
return decorator
๐ก Practical Examples
๐ Example 1: Shopping Cart Discount System
Letโs build something real:
# ๐๏ธ Discount decorator for shopping cart
def apply_discount(percentage):
"""๐ฐ Apply percentage discount to price calculation"""
def decorator(func):
def wrapper(*args, **kwargs):
original_price = func(*args, **kwargs)
discount_amount = original_price * (percentage / 100)
final_price = original_price - discount_amount
print(f"๐ต Original price: ${original_price:.2f}")
print(f"๐ซ Discount ({percentage}%): -${discount_amount:.2f}")
print(f"โจ Final price: ${final_price:.2f}")
return final_price
return wrapper
return decorator
# ๐ Shopping cart class
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, name, price, quantity=1):
"""โ Add item to cart"""
self.items.append({
'name': name,
'price': price,
'quantity': quantity,
'emoji': self._get_emoji(name)
})
print(f"โ
Added {quantity}x {name} to cart!")
def _get_emoji(self, name):
"""๐จ Get emoji for item"""
emojis = {
'book': '๐',
'coffee': 'โ',
'laptop': '๐ป',
'phone': '๐ฑ',
'pizza': '๐'
}
return emojis.get(name.lower(), '๐ฆ')
@apply_discount(20) # 20% discount!
def calculate_total(self):
"""๐ฐ Calculate total with discount"""
total = sum(item['price'] * item['quantity'] for item in self.items)
return total
def show_cart(self):
"""๐ Display cart contents"""
print("\n๐ Your Shopping Cart:")
for item in self.items:
print(f" {item['emoji']} {item['name']}: ${item['price']:.2f} x {item['quantity']}")
# ๐ฎ Let's use it!
cart = ShoppingCart()
cart.add_item("Coffee", 4.99, 2)
cart.add_item("Book", 19.99)
cart.add_item("Pizza", 12.99, 3)
cart.show_cart()
print("\n๐ณ Checkout:")
final_total = cart.calculate_total()
print(f"\n๐ You saved money with our discount!")
๐ฏ Try it yourself: Add a minimum_purchase
parameter to the discount decorator that only applies the discount if the total exceeds a certain amount!
๐ฎ Example 2: Game Achievement System
Letโs make it fun:
# ๐ Achievement decorator for games
def achievement(name, points, emoji="๐"):
"""๐ฏ Award achievement when function conditions are met"""
def decorator(func):
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
# Award achievement
if hasattr(self, 'achievements'):
if name not in [a['name'] for a in self.achievements]:
self.achievements.append({
'name': name,
'points': points,
'emoji': emoji
})
print(f"\n๐ ACHIEVEMENT UNLOCKED!")
print(f"{emoji} {name} (+{points} points)")
return result
return wrapper
return decorator
# ๐ฎ Game player class
class GamePlayer:
def __init__(self, name):
self.name = name
self.score = 0
self.level = 1
self.achievements = []
self.enemies_defeated = 0
print(f"๐ฎ Welcome, {name}! Let's play!")
@achievement("First Blood", 50, "๐ก๏ธ")
def defeat_enemy(self):
"""โ๏ธ Defeat an enemy"""
self.enemies_defeated += 1
self.score += 10
print(f"๐ฅ Enemy defeated! Total: {self.enemies_defeated}")
return True
@achievement("Level Up Master", 100, "๐")
@achievement("Rising Star", 75, "โญ")
def level_up(self):
"""๐ Level up the player"""
self.level += 1
self.score += 50
print(f"๐ LEVEL UP! You're now level {self.level}!")
return self.level
@achievement("Score Champion", 200, "๐")
def reach_score(self, target):
"""๐ฏ Reach a target score"""
if self.score >= target:
print(f"๐ Incredible! You've reached {target} points!")
return True
return False
def show_stats(self):
"""๐ Display player statistics"""
print(f"\n๐ {self.name}'s Stats:")
print(f" ๐ฏ Score: {self.score}")
print(f" ๐ Level: {self.level}")
print(f" โ๏ธ Enemies Defeated: {self.enemies_defeated}")
print(f" ๐ Achievements: {len(self.achievements)}")
if self.achievements:
print("\n๐ Achievements:")
for ach in self.achievements:
print(f" {ach['emoji']} {ach['name']} - {ach['points']} pts")
# ๐ฎ Play the game!
player = GamePlayer("Python Hero")
# Defeat some enemies
for i in range(3):
player.defeat_enemy()
# Level up
player.level_up()
# Check score achievement
player.reach_score(100)
# Show final stats
player.show_stats()
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Decorator Factories with Multiple Parameters
When youโre ready to level up, try this advanced pattern:
# ๐ฏ Advanced caching decorator
def cache(max_size=128, ttl=None):
"""๐พ Cache function results with size limit and TTL"""
import time
from collections import OrderedDict
def decorator(func):
cache_data = OrderedDict()
cache_time = {}
def wrapper(*args, **kwargs):
# Create cache key
key = str(args) + str(kwargs)
# Check if cached and not expired
if key in cache_data:
if ttl is None or (time.time() - cache_time[key]) < ttl:
print(f"๐พ Cache hit! Returning cached result")
return cache_data[key]
else:
print(f"โฐ Cache expired for key")
del cache_data[key]
del cache_time[key]
# Calculate result
print(f"๐ Computing result...")
result = func(*args, **kwargs)
# Store in cache
cache_data[key] = result
cache_time[key] = time.time()
# Enforce max size
if len(cache_data) > max_size:
oldest = next(iter(cache_data))
del cache_data[oldest]
del cache_time[oldest]
print(f"๐๏ธ Cache full, removed oldest entry")
return result
wrapper.cache_info = lambda: {
'size': len(cache_data),
'max_size': max_size,
'ttl': ttl
}
return wrapper
return decorator
# ๐ช Using the advanced cache
@cache(max_size=3, ttl=5) # 3 items max, 5 second TTL
def expensive_calculation(n):
"""๐งฎ Simulate expensive calculation"""
import time
time.sleep(1) # Simulate work
return n ** 2
# Test it out!
print(f"Result: {expensive_calculation(5)}") # Calculates
print(f"Result: {expensive_calculation(5)}") # From cache
print(f"Cache info: {expensive_calculation.cache_info()}")
๐๏ธ Advanced Topic 2: Class-based Decorators with Arguments
For the brave developers:
# ๐ Class-based decorator with arguments
class RateLimiter:
"""๐ฆ Rate limit function calls"""
def __init__(self, calls=5, period=60):
self.calls = calls
self.period = period
self.call_times = []
def __call__(self, func):
import time
def wrapper(*args, **kwargs):
now = time.time()
# Remove old calls outside the period
self.call_times = [t for t in self.call_times if now - t < self.period]
# Check rate limit
if len(self.call_times) >= self.calls:
wait_time = self.period - (now - self.call_times[0])
raise Exception(f"๐ซ Rate limit exceeded! Wait {wait_time:.1f}s")
# Record this call
self.call_times.append(now)
print(f"โ
Request allowed ({len(self.call_times)}/{self.calls})")
return func(*args, **kwargs)
return wrapper
# ๐จ Using class-based decorator
@RateLimiter(calls=3, period=10)
def api_request(endpoint):
"""๐ Simulate API request"""
print(f"๐ก Calling {endpoint}")
return f"Response from {endpoint}"
# Test rate limiting
try:
for i in range(5):
result = api_request(f"/api/data/{i}")
print(f"Got: {result}\n")
except Exception as e:
print(f"Error: {e}")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Losing Function Metadata
# โ Wrong way - loses function name and docstring!
def bad_decorator(param):
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # ๐ฐ Lost metadata!
return decorator
# โ
Correct way - preserve metadata!
from functools import wraps
def good_decorator(param):
def decorator(func):
@wraps(func) # ๐ก๏ธ Preserves metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
@good_decorator("test")
def my_function():
"""๐ This is my function"""
pass
print(f"Function name: {my_function.__name__}") # โ
Correct!
print(f"Docstring: {my_function.__doc__}") # โ
Preserved!
๐คฏ Pitfall 2: Mutable Default Arguments
# โ Dangerous - shared mutable default!
def bad_logger(log_list=[]): # ๐ฅ Shared between calls!
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
log_list.append(f"Called {func.__name__}")
print(f"๐ Log: {log_list}") # Keeps growing!
return result
return wrapper
return decorator
# โ
Safe - create new list each time!
def good_logger(log_list=None):
if log_list is None:
log_list = [] # ๐ก๏ธ Fresh list each time
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
log_list.append(f"Called {func.__name__}")
print(f"๐ Log: {log_list}")
return result
return wrapper
return decorator
๐ ๏ธ Best Practices
- ๐ฏ Use @wraps: Always preserve function metadata
- ๐ Clear Parameter Names: Make decorator arguments self-documenting
- ๐ก๏ธ Validate Arguments: Check decorator parameters are valid
- ๐จ Keep It Simple: Donโt over-complicate decorator logic
- โจ Document Well: Explain what parameters do
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Smart Validator System
Create a validation decorator system:
๐ Requirements:
- โ Type validation decorator (check parameter types)
- ๐ท๏ธ Range validation for numbers
- ๐ Length validation for strings
- ๐จ Custom validation with lambda functions
- ๐ Combine multiple validators on one function
๐ Bonus Points:
- Add custom error messages
- Support for validating multiple parameters
- Create a validation report
๐ก Solution
๐ Click to see solution
# ๐ฏ Our smart validation system!
from functools import wraps
def validate_type(**expected_types):
"""๐ก๏ธ Validate parameter types"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Get function signature
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
# Check each parameter
for param_name, expected_type in expected_types.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"โ {param_name} must be {expected_type.__name__}, "
f"got {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
def validate_range(param_name, min_val=None, max_val=None):
"""๐ Validate numeric range"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
if param_name in bound.arguments:
value = bound.arguments[param_name]
if min_val is not None and value < min_val:
raise ValueError(f"โ {param_name} must be >= {min_val}")
if max_val is not None and value > max_val:
raise ValueError(f"โ {param_name} must be <= {max_val}")
return func(*args, **kwargs)
return wrapper
return decorator
def validate_length(param_name, min_len=None, max_len=None):
"""๐ Validate string/list length"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
if param_name in bound.arguments:
value = bound.arguments[param_name]
length = len(value)
if min_len is not None and length < min_len:
raise ValueError(f"โ {param_name} must have length >= {min_len}")
if max_len is not None and length > max_len:
raise ValueError(f"โ {param_name} must have length <= {max_len}")
return func(*args, **kwargs)
return wrapper
return decorator
# ๐ฎ Test our validation system!
class UserRegistration:
def __init__(self):
self.users = []
@validate_type(username=str, age=int, email=str)
@validate_length("username", min_len=3, max_len=20)
@validate_range("age", min_val=18, max_val=120)
@validate_length("email", min_len=5)
def register_user(self, username, age, email):
"""๐ค Register a new user with validation"""
user = {
'username': username,
'age': age,
'email': email,
'emoji': '๐ง' if age < 30 else '๐ง'
}
self.users.append(user)
print(f"โ
User registered successfully!")
print(f"{user['emoji']} {username} (age {age})")
return user
# Test it out!
reg = UserRegistration()
# Valid registration
try:
reg.register_user("PythonFan", 25, "[email protected]")
except Exception as e:
print(f"Error: {e}")
# Invalid registrations
print("\n๐งช Testing validation:")
try:
reg.register_user("Jo", 25, "[email protected]") # Username too short
except Exception as e:
print(f"Caught: {e}")
try:
reg.register_user("ValidUser", 15, "[email protected]") # Too young
except Exception as e:
print(f"Caught: {e}")
try:
reg.register_user(12345, 25, "[email protected]") # Wrong type
except Exception as e:
print(f"Caught: {e}")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create decorators with arguments with confidence ๐ช
- โ Avoid common mistakes that trip up beginners ๐ก๏ธ
- โ Apply best practices in real projects ๐ฏ
- โ Debug decorator issues like a pro ๐
- โ Build flexible, reusable decorators with Python! ๐
Remember: Decorators with arguments are powerful tools that make your code more flexible and maintainable! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered decorators with arguments!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a logging system using decorators with arguments
- ๐ Move on to our next tutorial: Class Decorators
- ๐ Share your creative decorator implementations!
Remember: Every Python expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ