- Understand metaprogramming concepts and metaclasses
- Create custom metaclasses for class customization
- Implement dynamic class creation and modification
- Apply metaprogramming in real-world scenarios
Metaprogramming with Metaclasses
Introduction
Metaprogramming is the art of writing code that can manipulate, generate, or modify other code at runtime. It's like giving your program the ability to rewrite itself while it runs. In Python, metaclasses are the most powerful tool for metaprogramming, allowing you to control how classes are created and behave.
This is advanced Python territory - metaclasses can be complex, but they enable incredibly powerful patterns. Think of them as "class factories" that can customize class creation itself.
What is Metaprogramming?
Metaprogramming is writing code that manipulates other code. There are several levels:
1. Introspection
Examining code structure at runtime:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# Introspection
print(Person.__name__) # "Person"
print(Person.__dict__) # {'__init__': <function>, ...}
print(hasattr(Person, 'name')) # Check if attribute exists
2. Dynamic Code Execution
# Execute code from string
code = "def greet(name): return f'Hello, {name}!'"
exec(code)
print(greet("Alice")) # "Hello, Alice!"
# Evaluate expressions
result = eval("2 + 3 * 4")
print(result) # 14
3. Dynamic Class Creation
# Create class dynamically
MyClass = type('MyClass', (), {'x': 42, 'method': lambda self: self.x})
instance = MyClass()
print(instance.method()) # 42
Understanding Metaclasses
A metaclass is a class whose instances are classes. Just as a regular class defines how instances behave, a metaclass defines how classes behave.
The type Metaclass
type is Python's built-in metaclass. When you create a class:
class MyClass:
x = 42
def method(self):
return self.x
# This is equivalent to:
MyClass = type('MyClass', (), {'x': 42, 'method': lambda self: self.x})
The type() constructor takes:
- Class name (string)
- Base classes (tuple)
- Class attributes (dictionary)
Custom Metaclass
class MyMeta(type):
"""Custom metaclass that adds debugging info."""
def __new__(cls, name, bases, dct):
print(f"Creating class {name}")
print(f"Bases: {bases}")
print(f"Attributes: {list(dct.keys())}")
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=MyMeta):
x = 42
def method(self):
return self.x
# Output when class is defined:
# Creating class MyClass
# Bases: ()
# Attributes: ['__module__', '__qualname__', 'x', 'method']
Metaclass Methods
Metaclasses can override several methods to customize class creation:
new: Class Creation
class AutoRegister(type):
"""Metaclass that automatically registers classes."""
registry = {}
def __new__(cls, name, bases, dct):
# Create the class
new_class = super().__new__(cls, name, bases, dct)
# Register it
cls.registry[name] = new_class
return new_class
class Plugin(metaclass=AutoRegister):
pass
class FilePlugin(Plugin):
def process(self, file):
return f"Processing {file}"
class ImagePlugin(Plugin):
def process(self, image):
return f"Processing image {image}"
# All classes are automatically registered
print(AutoRegister.registry)
# {'Plugin': <class>, 'FilePlugin': <class>, 'ImagePlugin': <class>}
init: Class Initialization
class ValidateAttributes(type):
"""Metaclass that validates class attributes."""
def __init__(cls, name, bases, dct):
super().__init__(name, bases, dct)
# Validate required attributes
if not hasattr(cls, 'required_attr'):
raise TypeError(f"Class {name} must have 'required_attr'")
# Add default attributes
if not hasattr(cls, 'created_at'):
cls.created_at = "auto-generated"
class ValidClass(metaclass=ValidateAttributes):
required_attr = "present"
# created_at will be added automatically
print(ValidClass.created_at) # "auto-generated"
# This would raise TypeError
# class InvalidClass(metaclass=ValidateAttributes):
# pass
call: Instance Creation
class Singleton(type):
"""Metaclass that creates singleton classes."""
_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=Singleton):
def __init__(self, host):
self.host = host
print(f"Connecting to {host}")
# Only one instance ever created
db1 = DatabaseConnection("localhost")
db2 = DatabaseConnection("localhost")
print(db1 is db2) # True
Advanced Metaclass Patterns
Attribute Injection
class InjectAttributes(type):
"""Metaclass that injects attributes into classes."""
def __new__(cls, name, bases, dct):
# Add common attributes to all classes
dct['created_by_metaclass'] = True
dct['version'] = '1.0'
# Add methods dynamically
def get_info(self):
return f"{self.__class__.__name__} v{self.version}"
dct['get_info'] = get_info
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=InjectAttributes):
def __init__(self, value):
self.value = value
instance = MyClass(42)
print(instance.created_by_metaclass) # True
print(instance.get_info()) # "MyClass v1.0"
Method Modification
class AddLogging(type):
"""Metaclass that adds logging to all methods."""
def __new__(cls, name, bases, dct):
for attr_name, attr_value in dct.items():
if callable(attr_value) and not attr_name.startswith('_'):
# Wrap method with logging
original_method = attr_value
def logged_method(self, *args, **kwargs):
print(f"Calling {attr_name} on {self.__class__.__name__}")
result = original_method(self, *args, **kwargs)
print(f"{attr_name} returned: {result}")
return result
dct[attr_name] = logged_method
return super().__new__(cls, name, bases, dct)
class Calculator(metaclass=AddLogging):
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
calc = Calculator()
result = calc.add(3, 5) # Logs the method call
Class Validation
class StrictMeta(type):
"""Metaclass that enforces coding standards."""
def __new__(cls, name, bases, dct):
# Check for required docstring
if not dct.get('__doc__'):
raise TypeError(f"Class {name} must have a docstring")
# Check method naming conventions
for attr_name in dct:
if callable(dct[attr_name]) and not attr_name.startswith('_'):
if not attr_name.islower():
raise TypeError(f"Method {attr_name} should be lowercase")
# Check for required methods
required_methods = ['__str__', '__repr__']
for method in required_methods:
if method not in dct:
raise TypeError(f"Class {name} must implement {method}")
return super().__new__(cls, name, bases, dct)
class ValidClass(metaclass=StrictMeta):
"""This class follows all the rules."""
def __init__(self, value):
self.value = value
def __str__(self):
return f"ValidClass({self.value})"
def __repr__(self):
return f"ValidClass({self.value!r})"
def process_data(self): # lowercase method name
return self.value * 2
# This would raise TypeError due to missing docstring
# class InvalidClass(metaclass=StrictMeta):
# pass
Real-World Applications
ORM (Object-Relational Mapping)
class ModelMeta(type):
"""Metaclass for database models."""
def __new__(cls, name, bases, dct):
# Create table name from class name
if not hasattr(cls, 'table_name'):
dct['table_name'] = name.lower() + 's'
# Collect field definitions
fields = {}
for attr_name, attr_value in dct.items():
if isinstance(attr_value, Field):
fields[attr_name] = attr_value
dct['_fields'] = fields
# Add database methods
def save(self):
# Save to database logic
print(f"Saving {self.__class__.__name__} to {self.table_name}")
def delete(self):
# Delete from database logic
print(f"Deleting {self.__class__.__name__} from {self.table_name}")
dct['save'] = save
dct['delete'] = delete
return super().__new__(cls, name, bases, dct)
class Field:
"""Base field class."""
def __init__(self, field_type):
self.field_type = field_type
class CharField(Field):
def __init__(self, max_length):
super().__init__('VARCHAR')
self.max_length = max_length
class Model(metaclass=ModelMeta):
pass
class User(Model):
name = CharField(100)
email = CharField(255)
def __init__(self, name, email):
self.name = name
self.email = email
user = User("Alice", "alice@example.com")
user.save() # "Saving User to users"
user.delete() # "Deleting User to users"
Plugin System
class PluginMeta(type):
"""Metaclass for plugin system."""
plugins = {}
def __new__(cls, name, bases, dct):
new_class = super().__new__(cls, name, bases, dct)
# Register plugin if it has required methods
if hasattr(new_class, 'process') and callable(getattr(new_class, 'process')):
cls.plugins[name] = new_class
return new_class
class Plugin(metaclass=PluginMeta):
"""Base plugin class."""
pass
class TextPlugin(Plugin):
def process(self, data):
return data.upper()
class NumberPlugin(Plugin):
def process(self, data):
return data * 2
# All plugins are automatically registered
print(PluginMeta.plugins) # {'TextPlugin': <class>, 'NumberPlugin': <class>}
# Use plugins dynamically
def process_with_plugin(data, plugin_name):
plugin_class = PluginMeta.plugins[plugin_name]
plugin = plugin_class()
return plugin.process(data)
print(process_with_plugin("hello", "TextPlugin")) # "HELLO"
print(process_with_plugin(5, "NumberPlugin")) # 10
API Framework
class APIMeta(type):
"""Metaclass for REST API endpoints."""
def __new__(cls, name, bases, dct):
# Collect HTTP methods
methods = {}
for attr_name, attr_value in dct.items():
if hasattr(attr_value, '_http_method'):
methods[attr_value._http_method] = attr_value
dct['_http_methods'] = methods
# Create URL pattern
if not hasattr(cls, 'url_pattern'):
dct['url_pattern'] = f"/{name.lower()}"
return super().__new__(cls, name, bases, dct)
def http_method(method):
"""Decorator to mark HTTP methods."""
def decorator(func):
func._http_method = method
return func
return decorator
class APIEndpoint(metaclass=APIMeta):
pass
class UserAPI(APIEndpoint):
url_pattern = "/users"
@http_method('GET')
def list_users(self):
return ["Alice", "Bob", "Charlie"]
@http_method('POST')
def create_user(self, data):
return f"Created user: {data['name']}"
# Framework can now automatically route requests
print(UserAPI.url_pattern) # "/users"
print(UserAPI._http_methods) # {'GET': <function>, 'POST': <function>}
Metaclass vs Class Decorator
| Feature | Metaclass | Class Decorator |
|---|---|---|
| When applied | Class creation | After class creation |
| Can modify | Class structure | Class instances |
| Inheritance | Affects subclasses | Must be reapplied |
| Complexity | High | Low |
| Use case | Framework creation | Instance modification |
Best Practices
1. Use Metaclasses Sparingly
Metaclasses are powerful but complex. Only use them when:
- You need to modify class creation itself
- You're building a framework
- You need to enforce class-level constraints
2. Prefer Class Decorators When Possible
# Often better than a metaclass
def add_timestamps(cls):
cls.created_at = datetime.now()
cls.updated_at = datetime.now()
return cls
@add_timestamps
class MyModel:
pass
3. Document Metaclass Behavior
class MyMeta(type):
"""
Custom metaclass that enforces the following rules:
1. All classes must have a docstring
2. All methods must be lowercase
3. Required methods: __str__, __repr__
"""
# Implementation
4. Handle Inheritance Carefully
class BaseMeta(type):
def __new__(cls, name, bases, dct):
# Check if any base class already has this metaclass
for base in bases:
if isinstance(base, cls):
# Handle inheritance case
pass
return super().__new__(cls, name, bases, dct)
Key Points to Remember
- Metaclasses control how classes are created and behave
typeis the base metaclass for all Python classes__new__creates the class,__init__initializes it,__call__creates instances- Metaclasses are powerful but should be used sparingly
- Common uses: ORMs, plugin systems, API frameworks, validation
- Class decorators are often simpler alternatives for many use cases
Metaprogramming with metaclasses represents the pinnacle of Python's flexibility. In our next intermediate topic, we'll explore regular expressions - powerful text processing patterns that are essential for data validation, parsing, and manipulation in real-world applications.
