ch10s1_DecoratorsAndMetaclasses

Python is not just a language; it’s a *framework for creating frameworks*.

Chapter 10: Advanced Topics — Decorators and Metaclasses

🧠 Decorators and Metaclasses: Extending Python’s Core Behavior

Python is not just a language; it’s a framework for creating frameworks.
Two of its most powerful meta-programming tools are decorators and metaclasses, which allow you to modify behavior at runtime — without touching the core logic of your code.


🎁 1. Understanding Decorators — Functions That Modify Functions

A decorator is a higher-order function that takes another function and returns a modified version of it.
You can think of it as a wrapper that adds extra functionality before or after the original function executes.

Basic Decorator Example

def uppercase_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_decorator
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # Output: HELLO, ALICE!

💡 @decorator_name is shorthand for greet = decorator_name(greet)


⚙️ 2. The Anatomy of a Decorator

Decorators rely on first-class functions (functions as objects) and closures (nested functions that remember their enclosing scope).
This allows you to dynamically add features like logging, security, or performance tracking.

Example — Logging Decorator

from functools import wraps

def log_function_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_function_call
def add(a, b):
    return a + b

add(3, 5)

Output:

Calling function: add with args=(3, 5), kwargs={}
add returned 8

⚠️ Always use functools.wraps — it preserves function metadata like __name__ and __doc__.


⏱️ 3. Practical Decorators in Real Projects

3.1 Timing Function Execution

import time

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} executed in {end - start:.4f}s")
        return result
    return wrapper

@timer
def slow_task():
    time.sleep(1)
    return "Done!"

slow_task()

3.2 Access Control Example

def require_admin(func):
    @wraps(func)
    def wrapper(user_role, *args, **kwargs):
        if user_role != "admin":
            raise PermissionError("Access denied: admin required.")
        return func(user_role, *args, **kwargs)
    return wrapper

@require_admin
def delete_database(user_role):
    print("Database deleted!")

delete_database("admin")
# delete_database("guest")  → raises PermissionError

🧩 4. Class and Method Decorators

Decorators can also modify methods or entire classes.

Example — Class Method Decorator

def validate_positive(func):
    @wraps(func)
    def wrapper(self, value):
        if value < 0:
            raise ValueError("Value must be positive.")
        return func(self, value)
    return wrapper

class Account:
    def __init__(self, balance=0):
        self.balance = balance

    @validate_positive
    def deposit(self, amount):
        self.balance += amount
        print(f"New balance: {self.balance}")

🧱 5. Understanding Metaclasses — The Class of Classes

If decorators modify functions, metaclasses modify classes.
A metaclass defines how a class behaves — just as a class defines how its instances behave.

When you create a class:

class Foo:
    pass

Python internally does this:

Foo = type("Foo", (), {})

So, type is itself a metaclass!


🧬 6. Custom Metaclass Example

Singleton Pattern with Metaclass

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class DatabaseConnection(metaclass=SingletonMeta):
    def __init__(self, name):
        self.name = name

a = DatabaseConnection("main")
b = DatabaseConnection("replica")
print(a is b)  # True → both refer to same instance

💡 The __call__ method intercepts instance creation, letting you customize class instantiation behavior.


🧩 7. Advanced Metaclass — Auto-Registering Classes

You can use metaclasses to automatically register or modify classes at creation time.

class AutoRegister(type):
    registry = []

    def __new__(cls, name, bases, attrs):
        new_cls = super().__new__(cls, name, bases, attrs)
        cls.registry.append(name)
        return new_cls

class Base(metaclass=AutoRegister):
    pass

class User(Base): pass
class Admin(Base): pass

print(AutoRegister.registry)
# Output: ['Base', 'User', 'Admin']

🧰 8. Combining Decorators and Metaclasses

They can be combined to create powerful frameworks.

Example: auto-logging every method call in all classes of a metaclass.

def log_methods(cls):
    for attr, value in cls.__dict__.items():
        if callable(value):
            setattr(cls, attr, log_function_call(value))
    return cls

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

calc = Calculator()
calc.add(2, 3)

🧭 9. Decorators vs Metaclasses — When to Use Which

FeatureDecoratorMetaclass
TargetFunctions or ClassesClasses themselves
When AppliedAt function definition timeAt class creation time
Common Use CasesLogging, timing, validation, cachingSingleton, class registration, schema enforcement
ComplexitySimpleAdvanced
ScopeLocal (per function/class)Global (affects all class creation)

🧠 Rule of Thumb: Use decorators for behavior changes. Use metaclasses when you need to modify how classes are built.


🧪 10. Real-World Example — ORM-Style Validation

class FieldEnforcer(type):
    def __new__(mcls, name, bases, attrs):
        if "fields" in attrs:
            for field, ftype in attrs["fields"].items():
                if not isinstance(ftype, type):
                    raise TypeError(f"Invalid field type for {field}")
        return super().__new__(mcls, name, bases, attrs)

class Model(metaclass=FieldEnforcer):
    pass

class User(Model):
    fields = {"id": int, "name": str}

# Works fine
class InvalidModel(Model):
    fields = {"age": "integer"}  # Raises TypeError

💡 11. Best Practices

✅ Always use functools.wraps in decorators.
✅ Keep decorators small and modular — avoid “magic.”
✅ Use metaclasses sparingly — prefer composition and mixins first.
✅ Document all implicit behavior.
✅ Combine both tools for frameworks (e.g., Django ORM, Flask routing).


🧠 Summary

ConceptDescriptionExample
DecoratorFunction that wraps and modifies another function/class@timer, @log
MetaclassClass that defines behavior of other classesclass MyClass(metaclass=Meta)
Use CaseModify runtime behaviorCustomize class creation
Example LibrariesFlask, Django, FastAPISQLAlchemy, Pydantic

Decorators enhance behavior, metaclasses shape structure. Mastering both gives you true control over Python’s dynamic nature.