+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 409 of 541

๐Ÿ“˜ Decorators: Class and Method Decorators

Master decorators: class and method decorators in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿ’ŽAdvanced
25 min read

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:

  1. DRY Principle ๐Ÿ”„: Donโ€™t repeat yourself - apply common functionality once
  2. Separation of Concerns ๐Ÿ“ฆ: Keep business logic separate from cross-cutting concerns
  3. Clean Code ๐Ÿงน: Make your code more readable and maintainable
  4. 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

  1. ๐ŸŽฏ Use @wraps: Always preserve function metadata
  2. ๐Ÿ“ Document Decorators: Explain what your decorator does
  3. ๐Ÿ›ก๏ธ Handle Exceptions: Donโ€™t let decorators hide errors
  4. ๐ŸŽจ Keep It Simple: One decorator, one responsibility
  5. โœจ Make Decorators Reusable: Design for flexibility
  6. ๐Ÿ”„ Consider Performance: Cache when appropriate
  7. ๐Ÿ“Š 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:

  1. ๐Ÿ’ป Practice with the exercises above
  2. ๐Ÿ—๏ธ Add decorators to your existing projects
  3. ๐Ÿ“š Explore Pythonโ€™s built-in decorators (@property, @staticmethod, @classmethod)
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿš€โœจ