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: class and method decorators! ๐ In this guide, weโll explore how to use decorators to enhance your classes and methods with powerful functionality.
Youโll discover how decorators can transform your Python development experience. Whether youโre building web applications ๐, APIs ๐ฅ๏ธ, or libraries ๐, understanding class and method decorators is essential for writing elegant, maintainable code.
By the end of this tutorial, youโll feel confident using decorators to create clean, reusable code patterns in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Class and Method Decorators
๐ค What are Class and Method Decorators?
Decorators are like gift wrappers for your code ๐. Think of them as magical enhancements that you can apply to classes and methods to give them superpowers without modifying their core functionality.
In Python terms, decorators are functions that take another function or class as input and extend its behavior. This means you can:
- โจ Add logging or timing to methods automatically
- ๐ Validate inputs before a method runs
- ๐ก๏ธ Control access to methods based on permissions
- ๐ Cache expensive computations
๐ก Why Use Class and Method Decorators?
Hereโs why developers love decorators:
- DRY Principle ๐: Donโt repeat yourself - apply common functionality once
- Separation of Concerns ๐ฆ: Keep business logic separate from cross-cutting concerns
- Clean Code ๐งน: Make your code more readable and maintainable
- Flexible Enhancement ๐จ: Add or remove features without changing core code
Real-world example: Imagine building an e-commerce API ๐. With decorators, you can add authentication, logging, and rate limiting to all your endpoints without cluttering the actual business logic!
๐ง Basic Syntax and Usage
๐ Simple Method Decorator
Letโs start with a friendly example:
# ๐ Hello, Decorators!
import time
from functools import wraps
# ๐จ Creating a simple timing decorator
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time() # โฑ๏ธ Start the timer
result = func(*args, **kwargs)
end = time.time() # โน๏ธ Stop the timer
print(f"โฑ๏ธ {func.__name__} took {end - start:.2f} seconds")
return result
return wrapper
# ๐ฏ Using our decorator
class DataProcessor:
@measure_time
def process_data(self, data):
# ๐ Simulate some processing
time.sleep(0.1)
return [item * 2 for item in data]
# ๐ฎ Let's try it!
processor = DataProcessor()
result = processor.process_data([1, 2, 3, 4, 5])
print(f"๐ Result: {result}")
๐ก Explanation: The @measure_time
decorator wraps our method and automatically measures execution time. Notice how clean the actual method stays!
๐ฏ Class Decorator Pattern
Hereโs how to decorate entire classes:
# ๐๏ธ Class decorator for adding features
def add_debug_repr(cls):
"""Add a helpful __repr__ method 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
# ๐จ Another useful class decorator
def singleton(cls):
"""Ensure only one instance exists! ๐"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
# ๐ฎ Using class decorators
@add_debug_repr
@singleton
class DatabaseConnection:
def __init__(self, host="localhost", port=5432):
self.host = host
self.port = port
print(f"๐ Connecting to {host}:{port}")
# ๐งช Test it out!
db1 = DatabaseConnection()
db2 = DatabaseConnection() # Same instance!
print(f"Same instance? {db1 is db2} โ
")
print(db1) # Nice repr! ๐จ
๐ก Practical Examples
๐ Example 1: E-commerce Permission System
Letโs build something real:
# ๐๏ธ Permission decorator for e-commerce
from functools import wraps
from enum import Enum
class Role(Enum):
CUSTOMER = "customer"
ADMIN = "admin"
SELLER = "seller"
# ๐ญ Current user simulation
class User:
def __init__(self, name, role):
self.name = name
self.role = role
current_user = None # ๐ค Will be set during "login"
# ๐ Permission decorator
def requires_role(*allowed_roles):
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not current_user:
print("โ Error: Not logged in!")
return None
if current_user.role not in allowed_roles:
print(f"๐ซ Permission denied for {current_user.name}!")
return None
print(f"โ
Access granted for {current_user.name}")
return func(self, *args, **kwargs)
return wrapper
return decorator
# ๐ E-commerce system
class EcommerceSystem:
def __init__(self):
self.products = {
"1": {"name": "Python Book", "price": 29.99, "emoji": "๐"},
"2": {"name": "Coffee Mug", "price": 12.99, "emoji": "โ"},
"3": {"name": "Mechanical Keyboard", "price": 89.99, "emoji": "โจ๏ธ"}
}
self.orders = []
@requires_role(Role.CUSTOMER, Role.ADMIN)
def view_products(self):
print("\n๐๏ธ Available Products:")
for id, product in self.products.items():
print(f" {product['emoji']} {product['name']} - ${product['price']}")
@requires_role(Role.ADMIN)
def add_product(self, name, price, emoji="๐ฆ"):
new_id = str(len(self.products) + 1)
self.products[new_id] = {"name": name, "price": price, "emoji": emoji}
print(f"โจ Added new product: {emoji} {name}")
@requires_role(Role.ADMIN)
def view_all_orders(self):
print(f"\n๐ Total orders: {len(self.orders)}")
for order in self.orders:
print(f" ๐ฆ Order by {order['user']}: {order['items']}")
@requires_role(Role.CUSTOMER, Role.SELLER)
def place_order(self, product_ids):
items = [self.products[pid]['name'] for pid in product_ids if pid in self.products]
self.orders.append({"user": current_user.name, "items": items})
print(f"๐ Order placed successfully! Items: {items}")
# ๐ฎ Let's test our system!
shop = EcommerceSystem()
# ๐งช Test as customer
print("๐ค Testing as Customer:")
current_user = User("Alice", Role.CUSTOMER)
shop.view_products() # โ
Allowed
shop.place_order(["1", "2"]) # โ
Allowed
shop.add_product("New Item", 99.99) # โ Denied
# ๐งช Test as admin
print("\n๐ Testing as Admin:")
current_user = User("Bob", Role.ADMIN)
shop.add_product("Gaming Mouse", 59.99, "๐ฑ๏ธ") # โ
Allowed
shop.view_all_orders() # โ
Allowed
๐ฏ Try it yourself: Add a @log_action
decorator that records all method calls!
๐ฎ Example 2: Game State Management
Letโs make it fun with a game system:
# ๐ Game state management with decorators
import json
from datetime import datetime
# ๐พ Auto-save decorator
def auto_save(func):
"""Automatically save game state after important actions! ๐พ"""
@wraps(func)
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
self.save_game()
return result
return wrapper
# ๐ฏ Validate game state
def validate_state(func):
"""Ensure game state is valid before actions! ๐ก๏ธ"""
@wraps(func)
def wrapper(self, *args, **kwargs):
if self.player_health <= 0:
print("๐ Game Over! Cannot perform actions when dead!")
return None
if self.level < 1:
print("โ ๏ธ Invalid game state!")
return None
return func(self, *args, **kwargs)
return wrapper
# ๐ Achievement tracker
def achievement_tracker(achievement_name, condition_func):
"""Track and unlock achievements! ๐"""
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
if condition_func(self) and achievement_name not in self.achievements:
self.achievements.append(achievement_name)
print(f"๐ Achievement Unlocked: {achievement_name}!")
return result
return wrapper
return decorator
# ๐ฎ Our game class
@add_debug_repr # Reusing our earlier decorator!
class RPGGame:
def __init__(self, player_name):
self.player_name = player_name
self.level = 1
self.player_health = 100
self.experience = 0
self.inventory = ["๐ก๏ธ Wooden Sword", "๐ก๏ธ Basic Shield"]
self.achievements = ["๐ First Steps"]
self.gold = 50
print(f"๐ฎ Welcome to the adventure, {player_name}!")
@auto_save
@validate_state
@achievement_tracker("๐ฐ Rich Player", lambda self: self.gold >= 1000)
def collect_treasure(self, amount):
"""Find treasure! ๐"""
self.gold += amount
print(f"๐ฐ Found {amount} gold! Total: {self.gold}")
return self.gold
@auto_save
@validate_state
@achievement_tracker("โ๏ธ Dragon Slayer", lambda self: self.experience >= 1000)
def defeat_monster(self, monster_name, exp_reward, damage_taken):
"""Battle monsters! โ๏ธ"""
self.player_health -= damage_taken
self.experience += exp_reward
print(f"โ๏ธ Defeated {monster_name}!")
print(f"๐ Gained {exp_reward} XP | Health: {self.player_health}/100")
# Level up check
if self.experience >= self.level * 100:
self.level_up()
return self.experience
@auto_save
def level_up(self):
"""Level up! ๐"""
self.level += 1
self.player_health = 100 # Full heal on level up!
print(f"๐ LEVEL UP! You are now level {self.level}!")
print(f"๐ Health restored to full!")
def save_game(self):
"""Save game state ๐พ"""
save_data = {
"player": self.player_name,
"level": self.level,
"health": self.player_health,
"exp": self.experience,
"gold": self.gold,
"achievements": self.achievements,
"timestamp": datetime.now().isoformat()
}
# In real game, save to file
print(f"๐พ Game saved! (Level {self.level}, {self.gold} gold)")
# ๐ฎ Play the game!
game = RPGGame("Hero")
game.collect_treasure(100) # Find some gold
game.defeat_monster("๐บ Wolf", 50, 10) # Fight!
game.defeat_monster("๐ Dragon", 500, 30) # Epic battle!
game.collect_treasure(950) # Big treasure!
print(f"\n๐ Achievements: {', '.join(game.achievements)}")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Parameterized Decorators
When youโre ready to level up, try parameterized decorators:
# ๐ฏ Advanced caching decorator with TTL
import time
from functools import wraps
def cache_with_ttl(ttl_seconds=60):
"""Cache results with time-to-live! โฐ"""
def decorator(func):
cache = {}
cache_time = {}
@wraps(func)
def wrapper(*args, **kwargs):
# Create cache key
key = str(args) + str(kwargs)
# Check if cached and not expired
if key in cache:
if time.time() - cache_time[key] < ttl_seconds:
print(f"โจ Cache hit for {func.__name__}!")
return cache[key]
else:
print(f"โฐ Cache expired for {func.__name__}")
# Calculate and cache result
result = func(*args, **kwargs)
cache[key] = result
cache_time[key] = time.time()
return result
# Add cache control methods
wrapper.clear_cache = lambda: (cache.clear(), cache_time.clear())
wrapper.cache_info = lambda: {"size": len(cache), "keys": list(cache.keys())}
return wrapper
return decorator
# ๐ API client with caching
class WeatherAPI:
@cache_with_ttl(ttl_seconds=300) # 5 minute cache
def get_weather(self, city):
"""Fetch weather data (expensive operation) ๐ค๏ธ"""
print(f"๐ Fetching weather for {city}...")
time.sleep(1) # Simulate API call
# Mock weather data
return {
"city": city,
"temp": "72ยฐF",
"condition": "Sunny โ๏ธ",
"humidity": "45%"
}
@cache_with_ttl(ttl_seconds=3600) # 1 hour cache
def get_forecast(self, city, days=7):
"""Get extended forecast ๐
"""
print(f"๐ Fetching {days}-day forecast for {city}...")
time.sleep(2) # Simulate longer API call
return {"city": city, "days": days, "forecast": "Mostly sunny ๐"}
# ๐งช Test caching
api = WeatherAPI()
print(api.get_weather("New York")) # First call - slow
print(api.get_weather("New York")) # Cached - fast!
print(f"๐ Cache info: {api.get_weather.cache_info()}")
๐๏ธ Advanced Topic 2: Decorator Factories and Class Decorators
For the brave developers:
# ๐ Advanced decorator factory pattern
def create_validator(**validations):
"""Create custom validation decorators! ๐ก๏ธ"""
def decorator(func):
@wraps(func)
def wrapper(self, **kwargs):
# Validate each parameter
for param, validator in validations.items():
if param in kwargs:
value = kwargs[param]
if not validator(value):
raise ValueError(f"โ Invalid {param}: {value}")
return func(self, **kwargs)
return wrapper
return decorator
# ๐๏ธ Advanced class decorator with metaclass features
def dataclass_lite(cls):
"""Simple dataclass decorator! ๐ฆ"""
# Get annotations
annotations = getattr(cls, '__annotations__', {})
# Create __init__ method
def __init__(self, **kwargs):
for field, field_type in annotations.items():
if field in kwargs:
setattr(self, field, kwargs[field])
else:
# Set default values
default = None
if field_type == int:
default = 0
elif field_type == str:
default = ""
elif field_type == list:
default = []
setattr(self, field, default)
# Create __repr__ method
def __repr__(self):
fields = ', '.join(f"{k}={getattr(self, k)}" for k in annotations)
return f"{cls.__name__}({fields}) ๐ฏ"
# Create __eq__ method
def __eq__(self, other):
if not isinstance(other, cls):
return False
return all(getattr(self, k) == getattr(other, k) for k in annotations)
# Apply methods
cls.__init__ = __init__
cls.__repr__ = __repr__
cls.__eq__ = __eq__
return cls
# ๐ฎ Using our advanced decorators
@dataclass_lite
class Player:
name: str
level: int
health: int
mana: int
inventory: list
class GameCharacter:
@create_validator(
level=lambda x: 1 <= x <= 100,
health=lambda x: x >= 0,
name=lambda x: len(x) > 0
)
def update_stats(self, **stats):
"""Update character stats with validation! ๐"""
for stat, value in stats.items():
setattr(self, stat, value)
print(f"โ
Updated {stat} to {value}")
# ๐งช Test advanced features
player1 = Player(name="Hero", level=5, health=100)
player2 = Player(name="Hero", level=5, health=100)
print(player1) # Nice repr!
print(f"Equal? {player1 == player2}") # True!
character = GameCharacter()
character.level = 1
character.health = 100
character.name = "Warrior"
character.update_stats(level=10, health=150) # Valid
# character.update_stats(level=150) # Would raise error!
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Losing Method Metadata
# โ Wrong way - loses function metadata!
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # Lost __name__, __doc__, etc!
@bad_decorator
def important_function():
"""This documentation will be lost! ๐ข"""
pass
print(important_function.__name__) # 'wrapper' - Wrong!
# โ
Correct way - preserve metadata!
from functools import wraps
def good_decorator(func):
@wraps(func) # ๐ก๏ธ Preserves metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def important_function():
"""This documentation is preserved! ๐"""
pass
print(important_function.__name__) # 'important_function' - Correct!
๐คฏ Pitfall 2: Decorator Order Matters
# โ Wrong order - may not work as expected!
@measure_time
@validate_state # Validation happens AFTER timing starts
def process_data(self):
pass
# โ
Correct order - validate first, then time!
@validate_state # Check validity first
@measure_time # Then measure time
def process_data(self):
pass
# ๐ก Remember: decorators apply bottom-to-top!
๐ Pitfall 3: Class Decorators and Inheritance
# โ Problem - decorator may not work with inheritance
@singleton
class BaseClass:
pass
class DerivedClass(BaseClass): # ๐ฑ Not a singleton!
pass
# โ
Solution - apply decorator to each class
@singleton
class BaseClass:
pass
@singleton
class DerivedClass(BaseClass):
pass
๐ ๏ธ Best Practices
- ๐ฏ Use @wraps: Always preserve function metadata
- ๐ Document Decorators: Explain what your decorator does
- ๐ก๏ธ Handle Exceptions: Donโt let decorators hide errors
- ๐จ Keep It Simple: One decorator, one responsibility
- โจ Make Decorators Reusable: Design for flexibility
- ๐ Consider Performance: Cache when appropriate
- ๐ Add Introspection: Provide ways to inspect decorator state
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Web Framework Mini-Router
Create a mini web framework with decorators:
๐ Requirements:
- โ Route decorator to map URLs to methods
- ๐ท๏ธ Method decorators for GET/POST/PUT/DELETE
- ๐ค Authentication decorator
- ๐ Rate limiting decorator
- ๐จ JSON response decorator
๐ Bonus Points:
- Add middleware support
- Implement request validation
- Create response caching
๐ก Solution
๐ Click to see solution
# ๐ฏ Mini web framework with decorators!
from functools import wraps
import json
import time
from collections import defaultdict
# ๐ Route registry
routes = {
'GET': {},
'POST': {},
'PUT': {},
'DELETE': {}
}
# ๐ Rate limiting storage
rate_limit_storage = defaultdict(list)
# ๐จ Route decorator
def route(path, method='GET'):
def decorator(func):
routes[method][path] = func
return func
return decorator
# ๐ Authentication decorator
def requires_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
auth_token = request.get('headers', {}).get('Authorization')
if not auth_token or auth_token != 'Bearer secret-token':
return {
'status': 401,
'body': {'error': 'Unauthorized ๐ซ'}
}
return func(request, *args, **kwargs)
return wrapper
# ๐ฆ Rate limiting decorator
def rate_limit(max_requests=10, window_seconds=60):
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
client_ip = request.get('ip', 'unknown')
current_time = time.time()
# Clean old entries
rate_limit_storage[client_ip] = [
t for t in rate_limit_storage[client_ip]
if current_time - t < window_seconds
]
# Check rate limit
if len(rate_limit_storage[client_ip]) >= max_requests:
return {
'status': 429,
'body': {'error': 'Rate limit exceeded! ๐ฆ'}
}
# Record request
rate_limit_storage[client_ip].append(current_time)
return func(request, *args, **kwargs)
return wrapper
return decorator
# ๐ JSON response decorator
def json_response(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if isinstance(result, dict) and 'body' in result:
result['headers'] = result.get('headers', {})
result['headers']['Content-Type'] = 'application/json'
result['body'] = json.dumps(result['body'])
return result
return wrapper
# ๐ฎ Mini web application
class TodoAPI:
def __init__(self):
self.todos = []
self.next_id = 1
@route('/todos', 'GET')
@json_response
@rate_limit(max_requests=100)
def list_todos(self, request):
"""List all todos ๐"""
return {
'status': 200,
'body': {
'todos': self.todos,
'count': len(self.todos)
}
}
@route('/todos', 'POST')
@json_response
@requires_auth
@rate_limit(max_requests=10)
def create_todo(self, request):
"""Create a new todo โจ"""
data = request.get('body', {})
if 'title' not in data:
return {
'status': 400,
'body': {'error': 'Title required! ๐'}
}
todo = {
'id': self.next_id,
'title': data['title'],
'completed': False,
'emoji': data.get('emoji', '๐')
}
self.todos.append(todo)
self.next_id += 1
return {
'status': 201,
'body': {'message': 'Todo created! ๐', 'todo': todo}
}
@route('/todos/<id>', 'PUT')
@json_response
@requires_auth
def update_todo(self, request, id):
"""Update a todo โ๏ธ"""
todo_id = int(id)
data = request.get('body', {})
for todo in self.todos:
if todo['id'] == todo_id:
if 'completed' in data:
todo['completed'] = data['completed']
if 'title' in data:
todo['title'] = data['title']
return {
'status': 200,
'body': {'message': 'Updated! โ
', 'todo': todo}
}
return {
'status': 404,
'body': {'error': 'Todo not found! ๐'}
}
@route('/todos/<id>', 'DELETE')
@json_response
@requires_auth
def delete_todo(self, request, id):
"""Delete a todo ๐๏ธ"""
todo_id = int(id)
self.todos = [t for t in self.todos if t['id'] != todo_id]
return {
'status': 200,
'body': {'message': 'Deleted! ๐๏ธ'}
}
# ๐งช Test our framework
def handle_request(method, path, request=None):
"""Simple request handler ๐"""
request = request or {}
# Find matching route
for route_path, handler in routes[method].items():
if route_path == path:
return handler(api, request)
# Simple parameter matching
if '<' in route_path:
parts = route_path.split('/')
path_parts = path.split('/')
if len(parts) == len(path_parts):
params = {}
match = True
for i, part in enumerate(parts):
if part.startswith('<') and part.endswith('>'):
param_name = part[1:-1]
params[param_name] = path_parts[i]
elif part != path_parts[i]:
match = False
break
if match:
return handler(api, request, **params)
return {'status': 404, 'body': 'Not found ๐'}
# ๐ฎ Test the API!
api = TodoAPI()
# Public endpoints
print("๐ GET /todos:")
print(handle_request('GET', '/todos'))
# Authenticated endpoints
auth_request = {
'headers': {'Authorization': 'Bearer secret-token'},
'body': {'title': 'Learn decorators', 'emoji': '๐'}
}
print("\nโจ POST /todos (with auth):")
print(handle_request('POST', '/todos', auth_request))
# Test rate limiting
print("\n๐ฆ Testing rate limit:")
for i in range(12):
result = handle_request('GET', '/todos', {'ip': 'test-client'})
if result['status'] == 429:
print(f"Rate limited after {i} requests!")
break
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create method decorators with confidence ๐ช
- โ Build class decorators for powerful enhancements ๐๏ธ
- โ Use parameterized decorators for flexibility ๐ฏ
- โ Avoid common pitfalls that trip up beginners ๐ก๏ธ
- โ Apply best practices in real projects ๐
Remember: Decorators are powerful tools that make your code cleaner and more maintainable. Use them wisely! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered class and method decorators!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Add decorators to your existing projects
- ๐ Explore Pythonโs built-in decorators (@property, @staticmethod, @classmethod)
- ๐ Share your decorator creations with the community!
Remember: Every Python expert started with their first decorator. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ