foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • quiz
Python
  • Understand how class decorators work
  • Create and apply decorators to classes
  • Modify class behavior before instantiation
  • Implement singleton and other class-level patterns

Class Decorators

Introduction

Imagine you're a factory manager. Instead of modifying each machine individually as it comes off the assembly line, you install an automated enhancement system that upgrades every machine before it leaves the factory. The machines themselves don't change - they just get better capabilities.

Class decorators work similarly to function decorators, but they modify entire classes rather than individual functions. They run when the class is defined, allowing you to enhance or modify class behavior before any instances are created.


How Class Decorators Work

A class decorator is a function that takes a class as input and returns a modified version of that class. Unlike function decorators that wrap function calls, class decorators modify the class itself.

Basic Class Decorator

def add_greeting(cls):
    """Add a greeting method to any class."""
    cls.greet = lambda self: f"Hello from {cls.__name__}!"
    return cls

@add_greeting
class Person:
    def __init__(self, name):
        self.name = name

person = Person("Alice")
print(person.greet())  # "Hello from Person!"

Manual Application

class Animal:
    def __init__(self, species):
        self.species = species

def add_sound(cls):
    cls.make_sound = lambda self: f"{self.species} makes a sound!"
    return cls

# Manual decoration
EnhancedAnimal = add_sound(Animal)
dog = EnhancedAnimal("Dog")
print(dog.make_sound())  # "Dog makes a sound!"

Singleton Pattern with Class Decorators

One of the most common uses of class decorators is implementing the singleton pattern - ensuring only one instance of a class exists.

def singleton(cls):
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class DatabaseConnection:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        print(f"Connecting to database at {host}:{port}")
    
    def query(self, sql):
        return f"Executing: {sql}"

# All instances are actually the same object
db1 = DatabaseConnection("localhost", 5432)
db2 = DatabaseConnection("remote", 5432)

print(db1 is db2)  # True - same instance.
print(db1.query("SELECT * FROM users"))
print(db2.query("SELECT * FROM products"))

Adding Class Attributes and Methods

Class decorators can add new attributes and methods to classes dynamically.

def add_metadata(cls):
    """Add metadata attributes to a class."""
    cls.created_at = "2024-01-01"
    cls.version = "1.0"
    cls.author = "Python Developer"
    return cls

def add_logging(cls):
    """Add logging to all methods of a class."""
    for attr_name in dir(cls):
        attr = getattr(cls, attr_name)
        if callable(attr) and not attr_name.startswith('_'):
            setattr(cls, attr_name, log_method_calls(attr))
    return cls

def log_method_calls(method):
    def wrapper(*args, **kwargs):
        print(f"Calling {method.__name__}")
        result = method(*args, **kwargs)
        print(f"{method.__name__} returned: {result}")
        return result
    return wrapper

@add_metadata
@add_logging
class Calculator:
    def add(self, a, b):
        return a + b
    
    def multiply(self, a, b):
        return a * b

calc = Calculator()
print(f"Version: {Calculator.version}")
result = calc.add(5, 3)  # Logs the method call

Property Injection

Class decorators can add properties that behave like regular attributes but have custom logic.

def add_cached_property(cls):
    """Add a cached property to the class."""
    def cached_value(self):
        if not hasattr(self, '_cached_value'):
            print("Computing expensive operation...")
            self._cached_value = sum(range(1000000))  # Expensive operation
        return self._cached_value
    
    cls.expensive_property = property(cached_value)
    return cls

@add_cached_property
class DataProcessor:
    def __init__(self, data):
        self.data = data

processor = DataProcessor([1, 2, 3, 4, 5])
print(processor.expensive_property)  # Computes once
print(processor.expensive_property)  # Uses cached value

Validation and Constraints

Class decorators can enforce constraints on class creation or usage.

def require_attributes(*required_attrs):
    """Ensure class has required attributes."""
    def decorator(cls):
        original_init = cls.__init__
        
        def new_init(self, *args, **kwargs):
            original_init(self, *args, **kwargs)
            for attr in required_attrs:
                if not hasattr(self, attr):
                    raise AttributeError(f"Class {cls.__name__} must have attribute '{attr}'")
        
        cls.__init__ = new_init
        return cls
    return decorator

@require_attributes('name', 'age')
class Person:
    def __init__(self, name, age, email=None):
        self.name = name
        self.age = age
        self.email = email

# This works
person1 = Person("Alice", 30, "alice@example.com")

# This raises AttributeError
# person2 = Person("Bob", 25)  # Missing required attributes

Method Enhancement

Class decorators can modify or enhance existing methods.

def add_method_validation(cls):
    """Add input validation to methods."""
    for attr_name in dir(cls):
        attr = getattr(cls, attr_name)
        if callable(attr) and not attr_name.startswith('_'):
            # Wrap method with validation
            original_method = attr
            def validated_method(self, *args, **kwargs):
                # Add validation logic here
                if hasattr(self, 'validate_input'):
                    self.validate_input(*args, **kwargs)
                return original_method(self, *args, **kwargs)
            setattr(cls, attr_name, validated_method)
    return cls

@add_method_validation
class BankAccount:
    def __init__(self, balance):
        self.balance = balance
    
    def validate_input(self, amount, **kwargs):
        if amount < 0:
            raise ValueError("Amount cannot be negative")
    
    def deposit(self, amount):
        self.balance += amount
        return self.balance
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        self.balance -= amount
        return self.balance

account = BankAccount(1000)
account.deposit(500)    # Validated
# account.deposit(-100) # Raises ValueError

Real-World Applications

API Client with Automatic Retry

def add_retry_logic(max_retries=3):
    def decorator(cls):
        original_methods = {}
        
        # Store original methods
        for attr_name in dir(cls):
            attr = getattr(cls, attr_name)
            if callable(attr) and not attr_name.startswith('_'):
                original_methods[attr_name] = attr
        
        # Replace methods with retry logic
        for method_name, original_method in original_methods.items():
            def retry_method(self, *args, **kwargs):
                method = original_methods[method_name]
                for attempt in range(max_retries):
                    try:
                        return method(self, *args, **kwargs)
                    except Exception as e:
                        if attempt == max_retries - 1:
                            raise e
                        print(f"Attempt {attempt + 1} failed, retrying...")
                        time.sleep(1)
            
            setattr(cls, method_name, retry_method.__get__(None, cls))
        
        return cls
    return decorator

@add_retry_logic(max_retries=3)
class APIClient:
    def fetch_data(self, endpoint):
        # Simulate occasional failures
        if random.random() < 0.7:
            raise ConnectionError("Network error")
        return f"Data from {endpoint}"

client = APIClient()
data = client.fetch_data("/api/users")  # Automatically retries on failure

Data Class with Automatic Methods

def dataclass(cls):
    """Simple dataclass decorator that adds __repr__, __eq__, and __hash__."""
    
    def __repr__(self):
        attrs = [f"{k}={getattr(self, k)!r}" for k in self.__dict__]
        return f"{cls.__name__}({', '.join(attrs)})"
    
    def __eq__(self, other):
        if not isinstance(other, cls):
            return False
        return self.__dict__ == other.__dict__
    
    def __hash__(self):
        return hash(tuple(sorted(self.__dict__.items())))
    
    cls.__repr__ = __repr__
    cls.__eq__ = __eq__
    cls.__hash__ = __hash__
    return cls

@dataclass
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1)           # Point(x=1, y=2)
print(p1 == p2)     # True
print(p1 == p3)     # False
print(hash(p1))     # Can be used in sets

Class Decorator vs Function Decorator

Aspect Function Decorator Class Decorator
Input Function object Class object
Execution When function is called When class is defined
Modifies Function behavior Class structure
Use cases Logging, caching, validation Singletons, metadata, method injection

Best Practices

1. Preserve Class Identity

def my_class_decorator(cls):
    # Store original class for isinstance() checks
    cls.__original_class__ = cls
    # ... modifications ...
    return cls

2. Document Your Decorators

def singleton(description="Ensures only one instance exists"):
    """
    Singleton class decorator.
    
    Args:
        description: Description of the singleton behavior
        
    Returns:
        Decorated class that maintains single instance
    """
    def decorator(cls):
        # Implementation
        return cls
    return decorator

3. Consider Alternatives

Sometimes inheritance or metaclasses might be better than decorators:

# Instead of decorator, consider inheritance
class SingletonMeta(type):
    def __call__(cls, *args, **kwargs):
        # Singleton logic
        pass

class MyClass(metaclass=SingletonMeta):
    pass

Key Points to Remember

  • Class decorators modify entire classes, not just individual functions
  • They execute when the class is defined, before any instances exist
  • Common uses include singletons, method injection, and validation
  • Can add attributes, methods, or completely replace the class
  • Often used for cross-cutting concerns that affect entire classes
  • Consider alternatives like metaclasses for complex class modifications

Class decorators are powerful, but Python offers even more advanced decorator patterns. In the next lesson, we'll explore advanced decorators with functools and learn about decorator factories, nested decorators, and complex decorator patterns.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service