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'
# 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¶
- Use @property first: Simplest for computed attributes
- Avoid metaclasses: Use only when necessary (very rare!)
- Prefer composition: Mixins over multiple inheritance
- Use ABCs: Define clear interfaces
- Document heavily: Advanced OOP is hard to understand
-
Related Topics¶
- [01 Classes & Inheritance](/05-py3/02-object-oriented-patterns/(01-classes-inheritance/) - Class fundamentals
- 02 Decorators - Decorator patterns
- 03 Magic Methods - Special methods
- [05 Custom Bytecode & Metaprogramming](/05-py3/09-bytecode-and-execution/(05-custom-bytecode-metaprogramming/) - Metaprogramming