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 the magical world of Python decorators! ๐ In this tutorial, weโll explore how decorators can transform your functions with a functional programming approach.
Have you ever wanted to add superpowers to your functions without modifying their code? ๐ฆธโโ๏ธ Thatโs exactly what decorators do! Theyโre like gift wrappers ๐ that add extra functionality to your functions, making them more powerful, reusable, and elegant.
By the end of this tutorial, youโll be creating your own decorators with confidence and applying functional programming principles like a pro! Letโs dive in! ๐โโ๏ธ
๐ Understanding Decorators
๐ค What are Decorators?
Decorators are like magical enhancements ๐ช that you can apply to functions. Think of them as Instagram filters for your code - they take your original function and add extra features without changing its core purpose!
In Python terms, decorators are higher-order functions that take a function as input and return an enhanced version of that function. This means you can:
- โจ Add logging to any function
- ๐ Measure performance automatically
- ๐ก๏ธ Validate inputs before processing
- ๐ Add authentication checks
- โฐ Implement caching for expensive operations
๐ก Why Use the Functional Approach?
Hereโs why the functional approach to decorators is powerful:
- Pure Functions ๐: Create decorators without side effects
- Composability ๐๏ธ: Chain multiple decorators easily
- Immutability ๐: Preserve original function behavior
- Testability ๐งช: Easy to test in isolation
- Reusability โป๏ธ: Apply the same decorator to many functions
Real-world example: Imagine building a web API ๐. With decorators, you can add authentication, logging, and rate limiting to any endpoint with just a simple @
symbol!
๐ง Basic Syntax and Usage
๐ Simple Decorator Example
Letโs start with a friendly example:
# ๐ Hello, decorators!
def greet_decorator(func):
"""A decorator that adds a greeting! ๐"""
def wrapper(*args, **kwargs):
print("๐ Welcome! Let me help you with that...")
result = func(*args, **kwargs) # ๐ฏ Call original function
print("โจ All done! Have a great day!")
return result
return wrapper
# ๐จ Using the decorator
@greet_decorator
def calculate_sum(a, b):
"""Calculate the sum of two numbers ๐ข"""
result = a + b
print(f"๐ The sum of {a} and {b} is {result}")
return result
# ๐ฎ Let's try it!
calculate_sum(5, 3)
๐ก Explanation: The @greet_decorator
syntax is syntactic sugar for calculate_sum = greet_decorator(calculate_sum)
. Itโs Pythonโs way of making decorators beautiful! ๐จ
๐ฏ Common Decorator Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Timing decorator
import time
from functools import wraps
def time_it(func):
"""Measure function execution time โฑ๏ธ"""
@wraps(func) # ๐ก๏ธ Preserve function metadata
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
# ๐จ Pattern 2: Memoization decorator
def memoize(func):
"""Cache function results for efficiency ๐"""
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
print(f"๐ซ Found in cache!")
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
# ๐ Pattern 3: Decorator with parameters
def repeat(times):
"""Repeat function execution ๐"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
print(f"๐ฏ Attempt {i + 1}/{times}")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
๐ก Practical Examples
๐ Example 1: E-commerce Order Processing
Letโs build something real:
# ๐๏ธ E-commerce order processing with decorators
from functools import wraps
import json
from datetime import datetime
def log_transaction(func):
"""Log all transactions ๐"""
@wraps(func)
def wrapper(*args, **kwargs):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"๐ [{timestamp}] Starting {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"โ
[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"โ [{timestamp}] {func.__name__} failed: {str(e)}")
raise
return wrapper
def validate_order(func):
"""Validate order data ๐ก๏ธ"""
@wraps(func)
def wrapper(order_data, *args, **kwargs):
# ๐ Check required fields
required = ['customer_id', 'items', 'total']
missing = [field for field in required if field not in order_data]
if missing:
raise ValueError(f"โ Missing required fields: {missing}")
if not order_data['items']:
raise ValueError("๐ Order must contain at least one item!")
if order_data['total'] <= 0:
raise ValueError("๐ฐ Order total must be positive!")
print("โ
Order validation passed!")
return func(order_data, *args, **kwargs)
return wrapper
def discount(percentage):
"""Apply discount to order ๐ท๏ธ"""
def decorator(func):
@wraps(func)
def wrapper(order_data, *args, **kwargs):
original_total = order_data['total']
discount_amount = original_total * (percentage / 100)
order_data['total'] = original_total - discount_amount
order_data['discount_applied'] = percentage
print(f"๐ Applied {percentage}% discount!")
print(f"๐ต Original: ${original_total:.2f} โ New: ${order_data['total']:.2f}")
return func(order_data, *args, **kwargs)
return wrapper
return decorator
# ๐ฎ Using our decorators!
@log_transaction
@validate_order
@discount(20) # 20% off sale! ๐
def process_order(order_data):
"""Process an e-commerce order ๐ฆ"""
print(f"๐๏ธ Processing order for customer {order_data['customer_id']}")
print(f"๐ฆ Items: {len(order_data['items'])} products")
print(f"๐ณ Final total: ${order_data['total']:.2f}")
# Simulate processing
order_data['status'] = 'processed'
order_data['order_id'] = f"ORD-{datetime.now().strftime('%Y%m%d%H%M%S')}"
return order_data
# ๐ฏ Try it yourself!
order = {
'customer_id': 'CUST-123',
'items': ['๐ฑ iPhone', '๐ง AirPods', 'โ Apple Watch'],
'total': 1500.00
}
processed_order = process_order(order)
print(f"๐ Order {processed_order['order_id']} processed successfully!")
๐ฏ Try it yourself: Add a @retry
decorator that retries failed orders!
๐ฎ Example 2: Game Achievement System
Letโs make it fun:
# ๐ Achievement system with functional decorators
from functools import wraps, reduce
import time
def achievement_tracker(achievement_name, points):
"""Track player achievements ๐"""
def decorator(func):
@wraps(func)
def wrapper(player, *args, **kwargs):
result = func(player, *args, **kwargs)
# ๐ฏ Award achievement
if 'achievements' not in player:
player['achievements'] = []
achievement = {
'name': achievement_name,
'points': points,
'timestamp': time.time(),
'emoji': '๐'
}
player['achievements'].append(achievement)
player['total_points'] = player.get('total_points', 0) + points
print(f"๐ Achievement unlocked: {achievement_name} (+{points} points)!")
return result
return wrapper
return decorator
def combo_multiplier(multiplier):
"""Apply combo multiplier to scores ๐ฅ"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if isinstance(result, (int, float)):
combo_result = result * multiplier
print(f"๐ฅ Combo x{multiplier}! Score: {result} โ {combo_result}")
return combo_result
return result
return wrapper
return decorator
def performance_bonus(threshold):
"""Add bonus for high performance โก"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
execution_time = time.time() - start_time
if execution_time < threshold:
bonus = int(100 * (threshold - execution_time))
print(f"โก Speed bonus! Completed in {execution_time:.3f}s (+{bonus} points)")
return result + bonus if isinstance(result, (int, float)) else result
return result
return wrapper
return decorator
# ๐ฎ Create our game functions
@achievement_tracker("First Blood", 100)
@combo_multiplier(2)
@performance_bonus(0.5)
def defeat_enemy(player, enemy_type):
"""Defeat an enemy and earn points ๐ก๏ธ"""
base_points = {
'๐ง Zombie': 10,
'๐ Dragon': 50,
'๐น Boss': 100
}
points = base_points.get(enemy_type, 5)
print(f"๐ฅ Defeated {enemy_type}! Base points: {points}")
# Simulate combat time
time.sleep(0.3)
return points
# ๐ฏ Compose multiple decorators functionally
def compose(*decorators):
"""Compose multiple decorators functionally ๐จ"""
def decorator(func):
return reduce(lambda f, d: d(f), reversed(decorators), func)
return decorator
# ๐๏ธ Create a super-powered function
super_combo = compose(
achievement_tracker("Combo Master", 500),
combo_multiplier(5),
performance_bonus(0.2)
)
@super_combo
def ultimate_attack(player):
"""Unleash ultimate attack! ๐ซ"""
print("๐ ULTIMATE ATTACK ACTIVATED!")
time.sleep(0.1) # Fast execution for bonus
return 1000
# ๐ฎ Let's play!
player = {'name': 'PyMaster', 'total_points': 0}
# Battle sequence
score1 = defeat_enemy(player, '๐ง Zombie')
score2 = defeat_enemy(player, '๐ Dragon')
score3 = ultimate_attack(player)
print(f"\n๐ Final Stats for {player['name']}:")
print(f"๐ฏ Total Points: {player['total_points']}")
print(f"๐๏ธ Achievements: {len(player['achievements'])}")
๐ Advanced Concepts
๐งโโ๏ธ Functional Decorator Composition
When youโre ready to level up, try this advanced pattern:
# ๐ฏ Advanced functional composition
from functools import wraps, partial
from typing import Callable, Any
def curry_decorator(decorator: Callable) -> Callable:
"""Make decorators curryable ๐"""
@wraps(decorator)
def curried(*args, **kwargs):
if len(args) == 1 and callable(args[0]) and not kwargs:
# Direct decoration
return decorator(args[0])
else:
# Partial application
return partial(decorator, *args, **kwargs)
return curried
@curry_decorator
def rate_limit(func: Callable, calls: int = 1, period: float = 1.0) -> Callable:
"""Rate limit function calls ๐ฆ"""
call_times = []
@wraps(func)
def wrapper(*args, **kwargs):
current_time = time.time()
# Remove old calls outside the period
call_times[:] = [t for t in call_times if current_time - t < period]
if len(call_times) >= calls:
wait_time = period - (current_time - call_times[0])
raise Exception(f"๐ Rate limit exceeded! Wait {wait_time:.1f}s")
call_times.append(current_time)
return func(*args, **kwargs)
return wrapper
# ๐ช Using curried decorators
@rate_limit(calls=3, period=10.0)
def api_call(endpoint):
"""Simulate API call ๐"""
print(f"๐ก Calling {endpoint}")
return f"Response from {endpoint}"
# Or use it functionally
limited_func = rate_limit(calls=2)(lambda x: x * 2)
๐๏ธ Monadic Decorators
For the brave developers:
# ๐ Monadic decorator pattern
from typing import TypeVar, Generic, Callable, Optional
T = TypeVar('T')
U = TypeVar('U')
class Maybe(Generic[T]):
"""Maybe monad for safe operations ๐ก๏ธ"""
def __init__(self, value: Optional[T]):
self._value = value
def bind(self, func: Callable[[T], 'Maybe[U]']) -> 'Maybe[U]':
if self._value is None:
return Maybe(None)
return func(self._value)
def map(self, func: Callable[[T], U]) -> 'Maybe[U]':
if self._value is None:
return Maybe(None)
return Maybe(func(self._value))
@property
def value(self) -> Optional[T]:
return self._value
def maybe_decorator(func: Callable) -> Callable:
"""Wrap function result in Maybe monad ๐"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
return Maybe(result)
except Exception as e:
print(f"โ ๏ธ Operation failed: {e}")
return Maybe(None)
return wrapper
@maybe_decorator
def divide(a: float, b: float) -> float:
"""Safe division ๐ข"""
return a / b
# ๐ฎ Chain operations safely
result = (divide(10, 2)
.map(lambda x: x * 2)
.map(lambda x: x + 10)
.bind(lambda x: divide(x, 2)))
if result.value:
print(f"โจ Result: {result.value}")
else:
print("โ Computation failed somewhere in the chain")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Losing Function Metadata
# โ Wrong way - metadata gets lost!
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # ๐ฐ Lost __name__, __doc__, etc.
@bad_decorator
def my_function():
"""Important documentation"""
pass
print(my_function.__name__) # ๐ฅ Prints 'wrapper', not 'my_function'!
# โ
Correct way - preserve metadata!
from functools import wraps
def good_decorator(func):
@wraps(func) # ๐ก๏ธ Preserves function metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def my_function():
"""Important documentation"""
pass
print(my_function.__name__) # โ
Prints 'my_function'
print(my_function.__doc__) # โ
Prints 'Important documentation'
๐คฏ Pitfall 2: Decorator Order Matters
# โ Wrong order - authentication happens after logging!
@log_transaction # 2๏ธโฃ This runs second
@authenticate # 1๏ธโฃ This runs first
def sensitive_operation():
pass
# โ
Correct order - authenticate first, then log
@authenticate # 1๏ธโฃ Check auth first
@log_transaction # 2๏ธโฃ Only log if authenticated
def sensitive_operation():
pass
# ๐ก Remember: decorators apply bottom-to-top!
๐ ๏ธ Best Practices
- ๐ฏ Use @wraps: Always preserve function metadata
- ๐ Document Decorators: Explain what they do and when to use them
- ๐ก๏ธ Handle Exceptions: Donโt let decorators hide errors
- ๐จ Keep It Simple: One decorator, one responsibility
- โจ Make Them Composable: Design decorators to work together
- ๐ Consider Performance: Cache when appropriate
- ๐งช Test Thoroughly: Test decorators in isolation
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Caching System
Create a functional caching system with these features:
๐ Requirements:
- โ LRU (Least Recently Used) cache with size limit
- ๐ท๏ธ TTL (Time To Live) support for cache entries
- ๐ค Cache statistics (hits, misses, evictions)
- ๐ Cache warming capability
- ๐จ Decorator parameters for configuration
๐ Bonus Points:
- Add cache invalidation patterns
- Implement distributed cache support
- Create cache performance metrics
- Add async function support
๐ก Solution
๐ Click to see solution
# ๐ฏ Advanced caching system with functional approach!
from functools import wraps, lru_cache
from datetime import datetime, timedelta
from collections import OrderedDict
import asyncio
from typing import Any, Callable, Optional
class FunctionalCache:
"""Functional caching system ๐"""
def __init__(self, maxsize: int = 128, ttl: Optional[float] = None):
self.maxsize = maxsize
self.ttl = ttl
self.cache = OrderedDict()
self.stats = {
'hits': 0,
'misses': 0,
'evictions': 0
}
def get(self, key: Any) -> Optional[Any]:
"""Get item from cache ๐ฆ"""
if key in self.cache:
value, expiry = self.cache[key]
if expiry and datetime.now() > expiry:
# โฐ Expired entry
del self.cache[key]
return None
# ๐ฏ Move to end (LRU)
self.cache.move_to_end(key)
self.stats['hits'] += 1
return value
self.stats['misses'] += 1
return None
def set(self, key: Any, value: Any) -> None:
"""Set item in cache ๐พ"""
expiry = None
if self.ttl:
expiry = datetime.now() + timedelta(seconds=self.ttl)
if key in self.cache:
# Update existing
self.cache.move_to_end(key)
elif len(self.cache) >= self.maxsize:
# ๐๏ธ Evict oldest
self.cache.popitem(last=False)
self.stats['evictions'] += 1
self.cache[key] = (value, expiry)
def invalidate(self, pattern: Optional[Callable] = None) -> int:
"""Invalidate cache entries ๐งน"""
if pattern is None:
# Clear all
count = len(self.cache)
self.cache.clear()
return count
# Clear matching pattern
keys_to_remove = [k for k in self.cache if pattern(k)]
for key in keys_to_remove:
del self.cache[key]
return len(keys_to_remove)
def get_stats(self) -> dict:
"""Get cache statistics ๐"""
total = self.stats['hits'] + self.stats['misses']
hit_rate = self.stats['hits'] / total if total > 0 else 0
return {
**self.stats,
'size': len(self.cache),
'hit_rate': f"{hit_rate * 100:.1f}%",
'capacity': f"{len(self.cache)}/{self.maxsize}"
}
def cached(maxsize: int = 128, ttl: Optional[float] = None):
"""Functional cache decorator ๐จ"""
cache = FunctionalCache(maxsize=maxsize, ttl=ttl)
def decorator(func: Callable) -> Callable:
@wraps(func)
def sync_wrapper(*args, **kwargs):
# ๐ Create cache key
key = (args, tuple(sorted(kwargs.items())))
# ๐ฆ Check cache
result = cache.get(key)
if result is not None:
print(f"๐ซ Cache hit for {func.__name__}!")
return result
# ๐ฏ Execute function
result = func(*args, **kwargs)
cache.set(key, result)
return result
@wraps(func)
async def async_wrapper(*args, **kwargs):
# ๐ Create cache key
key = (args, tuple(sorted(kwargs.items())))
# ๐ฆ Check cache
result = cache.get(key)
if result is not None:
print(f"๐ซ Cache hit for {func.__name__}!")
return result
# ๐ฏ Execute async function
result = await func(*args, **kwargs)
cache.set(key, result)
return result
# ๐ Add cache control methods
wrapper = async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
wrapper.cache = cache
wrapper.invalidate = cache.invalidate
wrapper.stats = cache.get_stats
return wrapper
return decorator
# ๐ฎ Test our caching system!
@cached(maxsize=10, ttl=5.0)
def expensive_calculation(n: int) -> int:
"""Simulate expensive calculation ๐งฎ"""
print(f"๐ง Computing factorial of {n}...")
result = 1
for i in range(1, n + 1):
result *= i
return result
@cached(maxsize=5)
async def fetch_data(url: str) -> str:
"""Simulate async data fetching ๐"""
print(f"๐ก Fetching from {url}...")
await asyncio.sleep(1) # Simulate network delay
return f"Data from {url}"
# ๐ฏ Demo time!
print("๐ Testing synchronous caching:")
print(f"Result: {expensive_calculation(5)}") # Miss
print(f"Result: {expensive_calculation(5)}") # Hit!
print(f"Result: {expensive_calculation(6)}") # Miss
print(f"\n๐ Cache stats: {expensive_calculation.stats()}")
# ๐งน Invalidate specific entries
expensive_calculation.invalidate(lambda key: key[0][0] > 5)
print("\n๐ Testing async caching:")
async def test_async():
await fetch_data("api.example.com") # Miss
await fetch_data("api.example.com") # Hit!
print(f"๐ Async cache stats: {fetch_data.stats()}")
# Run async test
asyncio.run(test_async())
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create functional decorators with confidence ๐ช
- โ Compose decorators for powerful combinations ๐ก๏ธ
- โ Apply functional principles to decorator design ๐ฏ
- โ Debug decorator issues like a pro ๐
- โ Build reusable decorator libraries with Python! ๐
Remember: Decorators are just functions that transform functions. Keep them simple, pure, and composable! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered decorators with a functional approach!
Hereโs what to do next:
- ๐ป Practice with the caching exercise above
- ๐๏ธ Build a decorator library for your projects
- ๐ Explore functools and decorator modules
- ๐ Share your decorator creations with the community!
Remember: Every Python expert started with simple decorators. Keep experimenting, keep learning, and most importantly, have fun with functional programming! ๐
Happy decorating! ๐๐โจ