Skip to content

Metaclasses & Advanced OOP

Overview

Metaclasses control how classes are created: - Classes are objects: Created by metaclasses - new and init: Creating and initializing classes - Method registration: Auto-register methods - Validation: Enforce class design rules - Implications for ML: Framework architecture, auto-differentiation decorators


What Are Metaclasses?

The Metaclass Hierarchy

# Classes are instances of their metaclass
class Meta(type):
    """A metaclass."""
    pass

class MyClass(metaclass=Meta):
    """A class created by Meta."""
    pass

# The type of MyClass is Meta
print(type(MyClass))  # <class 'Meta'>

# The type of Meta is type
print(type(Meta))     # <class 'type'>

# All classes by default use 'type' as metaclass
class NormalClass:
    pass

print(type(NormalClass))  # <class 'type'>

Creating Classes Dynamically

Using type()

# Define a class dynamically
MyDynamicClass = type('MyDynamicClass', (object,), {
    'x': 10,
    'method': lambda self: self.x * 2
})

# Create instance
obj = MyDynamicClass()
print(obj.x)          # 10
print(obj.method())   # 20

# Equivalent to:
class MyDynamicClass2:
    x = 10
    def method(self):
        return self.x * 2

Metaclass new and init

class Meta(type):
    """Metaclass that prints when class is created."""

    def __new__(mcs, name, bases, namespace):
        print(f"Creating class {name}")

        # Add class variable
        namespace['created_by'] = 'Meta'

        # Create the class
        cls = super().__new__(mcs, name, bases, namespace)
        return cls

    def __init__(cls, name, bases, namespace):
        print(f"Initializing class {name}")
        super().__init__(name, bases, namespace)

class MyClass(metaclass=Meta):
    """Defined with Meta metaclass."""
    pass

# Output:
# Creating class MyClass
# Initializing class MyClass

Practical Metaclass Patterns

Pattern 1: Singleton Metaclass

class SingletonMeta(type):
    """Metaclass that ensures only one instance."""

    _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):
        print("Creating database connection")
        self.connection = "Connected"

# Only one instance is created
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True - same object!

Pattern 2: Method Registration

class RegistryMeta(type):
    """Metaclass that auto-registers methods."""

    def __new__(mcs, name, bases, namespace):
        namespace['_registry'] = {}

        # Register methods starting with 'handler_'
        for key, value in namespace.items():
            if callable(value) and key.startswith('handler_'):
                event_name = key[8:]  # Remove 'handler_'
                namespace['_registry'][event_name] = value

        return super().__new__(mcs, name, bases, namespace)

class EventHandler(metaclass=RegistryMeta):
    def handler_click(self, event):
        print(f"Handling click: {event}")

    def handler_hover(self, event):
        print(f"Handling hover: {event}")

    def regular_method(self):
        print("Not registered")

# Check registry
print(EventHandler._registry)
# {'click': <function>, 'hover': <function>}

# Use it
handler = EventHandler()
for event_name in EventHandler._registry:
    print(f"Registered: {event_name}")

Pattern 3: Validation Metaclass

class ValidatedMeta(type):
    """Metaclass that validates class structure."""

    def __new__(mcs, name, bases, namespace):
        # Ensure all methods have docstrings
        for key, value in namespace.items():
            if callable(value) and not key.startswith('_'):
                if not value.__doc__:
                    raise TypeError(f"Method {key} must have a docstring")

        return super().__new__(mcs, name, bases, namespace)

class WellDocumented(metaclass=ValidatedMeta):
    def method_with_doc(self):
        """This has a docstring."""
        pass

# This would raise TypeError:
# class BadDocumented(metaclass=ValidatedMeta):
#     def method_without_doc(self):
#         pass  # No docstring!

Advanced OOP Patterns

Abstract Base Classes

from abc import ABC, abstractmethod

class DataLoader(ABC):
    """Abstract base class for data loaders."""

    @abstractmethod
    def load(self):
        """Load data."""
        pass

    @abstractmethod
    def __iter__(self):
        """Iterate over data."""
        pass

class CSVLoader(DataLoader):
    def load(self):
        return "CSV data"

    def __iter__(self):
        return iter([1, 2, 3])

# Cannot instantiate abstract class
# loader = DataLoader()  # TypeError!

# Can instantiate concrete class
csv_loader = CSVLoader()
print(csv_loader.load())

Mixins for Composition

class TimingMixin:
    """Mixin to add timing to methods."""

    def timed(self, func):
        def wrapper(*args, **kwargs):
            import time
            start = time.time()
            result = func(*args, **kwargs)
            elapsed = time.time() - start
            print(f"{func.__name__} took {elapsed:.4f}s")
            return result
        return wrapper

class LoggingMixin:
    """Mixin to add logging."""

    def log(self, message):
        print(f"[LOG] {message}")

class Model(TimingMixin, LoggingMixin):
    def forward(self, x):
        self.log("Forward pass started")
        return x * 2

model = Model()
print(model.forward(5))

Real-World ML Examples

PyTorch Module Metaclass Pattern

class ModuleMeta(type):
    """Mimics PyTorch Module registration."""

    def __new__(mcs, name, bases, namespace):
        # Collect parameters and modules
        namespace['_parameters'] = {}
        namespace['_modules'] = {}

        for key, value in namespace.items():
            if hasattr(value, '__call__') and hasattr(value, 'weight'):
                # Register as parameter
                namespace['_parameters'][key] = value

        return super().__new__(mcs, name, bases, namespace)

class Layer(metaclass=ModuleMeta):
    def __init__(self, in_features, out_features):
        self.weight = [1.0] * out_features
        self.bias = [0.0] * out_features

# Automatically registered
print(Layer._parameters)  # Lists registered parameters

JAX PyTree Registration

class PyTreeMeta(type):
    """Metaclass for registering custom PyTree types."""

    _pytrees = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)

        # Register custom pytree
        if 'flatten' in namespace and 'unflatten' in namespace:
            mcs._pytrees[name] = cls

        return cls

class CustomArray(metaclass=PyTreeMeta):
    def __init__(self, data):
        self.data = data

    def flatten(self):
        return self.data, None

    @classmethod
    def unflatten(cls, aux, data):
        return cls(data)

# Automatically registered for JAX
print(PyTreeMeta._pytrees)

Summary: When to Use What

Pattern Use Case Complexity
@property Computed attributes Low
Descriptors Attribute access control Medium
ABC (Abstract) Define interfaces Low
Mixins Code reuse Medium
Metaclasses Control class creation High

Best Practices

  1. Use @property first: Simplest for computed attributes
  2. Avoid metaclasses: Use only when necessary (very rare!)
  3. Prefer composition: Mixins over multiple inheritance
  4. Use ABCs: Define clear interfaces
  5. Document heavily: Advanced OOP is hard to understand