Python Metaprogramming Explained
Basic Concepts of Metaprogramming
Metaprogramming refers to writing code that can manipulate, generate, or modify code. Python provides rich metaprogramming tools, including decorators, metaclasses, dynamic attributes, etc.
Metaprogramming Use Cases
- Framework development (e.g., Django ORM)
- Code generation and automation
- Dynamic property and method creation
- Aspect-Oriented Programming (AOP)
- Serialization and deserialization
Metaclasses
What is a Metaclass
A metaclass is a class that creates classes, just as a class is a template for creating objects, a metaclass is a template for creating classes.
python# Basic concept class MyClass: pass # MyClass is an instance of type print(type(MyClass)) # <class 'type'> # obj is an instance of MyClass obj = MyClass() print(type(obj)) # <class '__main__.MyClass'>
Custom Metaclasses
pythonclass MyMeta(type): def __new__(cls, name, bases, namespace): # Execute when class is created print(f"Creating class: {name}") # Add class attribute namespace['created_by'] = 'MyMeta' return super().__new__(cls, name, bases, namespace) class MyClass(metaclass=MyMeta): pass print(MyClass.created_by) # MyMeta
Metaclass Applications
pythonclass SingletonMeta(type): """Singleton metaclass""" _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls] class Singleton(metaclass=SingletonMeta): def __init__(self, value): self.value = value s1 = Singleton(1) s2 = Singleton(2) print(s1 is s2) # True print(s1.value) # 2
Advanced Metaclass Usage
pythonclass ValidateMeta(type): """Validation metaclass""" def __new__(cls, name, bases, namespace): # Ensure class has specific attributes if 'required_attr' not in namespace: raise TypeError(f"{name} must have 'required_attr'") # Validate methods for attr_name, attr_value in namespace.items(): if callable(attr_value) and not attr_name.startswith('_'): if not hasattr(attr_value, '__annotations__'): raise TypeError(f"Method {attr_name} must have type hints") return super().__new__(cls, name, bases, namespace) class ValidatedClass(metaclass=ValidateMeta): required_attr = "value" def method(self, x: int) -> int: return x * 2 # class InvalidClass(metaclass=ValidateMeta): # pass # TypeError: InvalidClass must have 'required_attr'
Dynamic Properties and Methods
Dynamic Properties
pythonclass DynamicAttributes: def __init__(self): self._data = {} def __getattr__(self, name): """Called when accessing non-existent attributes""" if name.startswith('get_'): attr_name = name[4:] return self._data.get(attr_name) raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") def __setattr__(self, name, value): """Called when setting attributes""" if name.startswith('_'): super().__setattr__(name, value) else: self._data[name] = value def __delattr__(self, name): """Called when deleting attributes""" if name in self._data: del self._data[name] else: super().__delattr__(name) obj = DynamicAttributes() obj.name = "Alice" obj.age = 25 print(obj.get_name) # Alice print(obj.get_age) # 25
Dynamic Methods
pythonclass DynamicMethods: def __init__(self): self.methods = {} def add_method(self, name, func): """Dynamically add methods""" self.methods[name] = func def __getattr__(self, name): """Dynamically call methods""" if name in self.methods: return self.methods[name] raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") obj = DynamicMethods() # Dynamically add methods obj.add_method('greet', lambda self, name: f"Hello, {name}!") obj.add_method('calculate', lambda self, x, y: x + y) print(obj.greet("Alice")) # Hello, Alice! print(obj.calculate(3, 5)) # 8
Creating Methods with types Module
pythonimport types class MyClass: pass def new_method(self): return "This is a dynamically added method" # Dynamically add method MyClass.new_method = new_method obj = MyClass() print(obj.new_method()) # This is a dynamically added method # Using types.MethodType def another_method(self, value): return f"Value: {value}" obj.another_method = types.MethodType(another_method, obj) print(obj.another_method(42)) # Value: 42
Descriptors
Descriptor Protocol
Descriptors are classes that implement __get__, __set__, and __delete__ methods, used to control attribute access.
pythonclass Descriptor: def __init__(self, name=None): self.name = name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__.get(self.name, f"No {self.name} set") def __set__(self, instance, value): instance.__dict__[self.name] = value def __delete__(self, instance): if self.name in instance.__dict__: del instance.__dict__[self.name] class Person: name = Descriptor('name') age = Descriptor('age') person = Person() person.name = "Alice" person.age = 25 print(person.name) # Alice print(person.age) # 25
Descriptor Applications
pythonclass ValidatedAttribute: """Validated attribute descriptor""" def __init__(self, validator=None, default=None): self.validator = validator self.default = default self.name = None def __set_name__(self, owner, name): self.name = f"_{name}" def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self.name, self.default) def __set__(self, instance, value): if self.validator and not self.validator(value): raise ValueError(f"Invalid value for {self.name}: {value}") setattr(instance, self.name, value) class User: name = ValidatedAttribute(lambda x: isinstance(x, str) and len(x) > 0) age = ValidatedAttribute(lambda x: isinstance(x, int) and 0 <= x <= 150) email = ValidatedAttribute(lambda x: '@' in x) user = User() user.name = "Alice" user.age = 25 user.email = "alice@example.com" print(user.name) # Alice print(user.age) # 25 # user.age = -5 # ValueError: Invalid value for _age: -5
Property Decorators
@property Decorator
pythonclass Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): """Get Celsius temperature""" return self._celsius @celsius.setter def celsius(self, value): """Set Celsius temperature""" if value < -273.15: raise ValueError("Temperature below absolute zero") self._celsius = value @property def fahrenheit(self): """Get Fahrenheit temperature (read-only)""" return self._celsius * 9/5 + 32 temp = Temperature(25) print(temp.celsius) # 25 print(temp.fahrenheit) # 77.0 temp.celsius = 30 print(temp.celsius) # 30 # temp.fahrenheit = 100 # AttributeError: can't set attribute
Dynamic Property Calculation
pythonclass Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value <= 0: raise ValueError("Radius must be positive") self._radius = value @property def diameter(self): return self._radius * 2 @property def area(self): return 3.14159 * self._radius ** 2 @property def circumference(self): return 2 * 3.14159 * self._radius circle = Circle(5) print(circle.diameter) # 10 print(circle.area) # 78.53975 print(circle.circumference) # 31.4159
Dynamic Class Creation
Creating Classes with type
python# Dynamically create class def __init__(self, name): self.name = name def greet(self): return f"Hello, {self.name}!" # Create class using type DynamicClass = type( 'DynamicClass', (object,), { '__init__': __init__, 'greet': greet, 'class_var': 'dynamic' } ) obj = DynamicClass("Alice") print(obj.greet()) # Hello, Alice! print(obj.class_var) # dynamic
Dynamically Creating Subclasses
pythondef create_subclass(base_class, subclass_name, extra_methods=None): """Dynamically create subclass""" namespace = extra_methods or {} return type(subclass_name, (base_class,), namespace) class Base: def base_method(self): return "Base method" # Dynamically create subclass extra_methods = { 'extra_method': lambda self: "Extra method" } SubClass = create_subclass(Base, 'SubClass', extra_methods) obj = SubClass() print(obj.base_method()) # Base method print(obj.extra_method()) # Extra method
Class Decorators
Basic Class Decorator
pythondef add_class_method(cls): """Decorator to add class methods""" @classmethod def class_method(cls): return f"Class method of {cls.__name__}" cls.class_method = class_method return cls @add_class_method class MyClass: pass print(MyClass.class_method()) # Class method of MyClass
Class Decorator Applications
pythondef singleton(cls): """Singleton class decorator""" instances = {} def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @singleton class Database: def __init__(self): self.connection = "Connected" db1 = Database() db2 = Database() print(db1 is db2) # True
Parameterized Class Decorators
pythondef add_attributes(**attrs): """Decorator to add class attributes""" def decorator(cls): for name, value in attrs.items(): setattr(cls, name, value) return cls return decorator @add_attributes(version="1.0", author="Alice") class MyClass: pass print(MyClass.version) # 1.0 print(MyClass.author) # Alice
Practical Application Scenarios
1. ORM Framework
pythonclass Field: """Field descriptor""" def __init__(self, field_type, primary_key=False): self.field_type = field_type self.primary_key = primary_key self.name = None def __set_name__(self, owner, name): self.name = name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__.get(self.name) def __set__(self, instance, value): if not isinstance(value, self.field_type): raise TypeError(f"Expected {self.field_type}, got {type(value)}") instance.__dict__[self.name] = value class ModelMeta(type): """Model metaclass""" def __new__(cls, name, bases, namespace): # Collect fields fields = {} for key, value in namespace.items(): if isinstance(value, Field): fields[key] = value namespace['_fields'] = fields return super().__new__(cls, name, bases, namespace) class Model(metaclass=ModelMeta): def __init__(self, **kwargs): for name, value in kwargs.items(): setattr(self, name, value) class User(Model): id = Field(int, primary_key=True) name = Field(str) age = Field(int) user = User(id=1, name="Alice", age=25) print(user.name) # Alice print(user.age) # 25
2. API Response Validation
pythonclass ValidatedResponse: """Validated response class""" def __init__(self, schema): self.schema = schema def __call__(self, cls): def __init__(self, data): self.validate(data) for key, value in data.items(): setattr(self, key, value) def validate(self, data): for field, field_type in self.schema.items(): if field not in data: raise ValueError(f"Missing field: {field}") if not isinstance(data[field], field_type): raise TypeError(f"Invalid type for {field}") cls.__init__ = __init__ cls.validate = validate return cls @ValidatedResponse({'name': str, 'age': int, 'email': str}) class UserResponse: pass user_data = {'name': 'Alice', 'age': 25, 'email': 'alice@example.com'} user = UserResponse(user_data) print(user.name) # Alice
3. Dynamic Form Generation
pythonclass FormField: """Form field""" def __init__(self, field_type, required=False, default=None): self.field_type = field_type self.required = required self.default = default self.name = None def __set_name__(self, owner, name): self.name = name def validate(self, value): if self.required and value is None: raise ValueError(f"{self.name} is required") if value is not None and not isinstance(value, self.field_type): raise TypeError(f"Invalid type for {self.name}") return True class FormMeta(type): """Form metaclass""" def __new__(cls, name, bases, namespace): fields = {} for key, value in namespace.items(): if isinstance(value, FormField): fields[key] = value namespace['_fields'] = fields return super().__new__(cls, name, bases, namespace) class Form(metaclass=FormMeta): def __init__(self, **kwargs): for name, field in self._fields.items(): value = kwargs.get(name, field.default) field.validate(value) setattr(self, name, value) def to_dict(self): return {name: getattr(self, name) for name in self._fields} class UserForm(Form): name = FormField(str, required=True) age = FormField(int, default=18) email = FormField(str, required=True) form = UserForm(name="Alice", email="alice@example.com") print(form.to_dict()) # {'name': 'Alice', 'age': 18, 'email': 'alice@example.com'}
Best Practices
1. Use Metaclasses Cautiously
python# Bad practice - Overusing metaclasses class ComplexMeta(type): def __new__(cls, name, bases, namespace): # Complex metaclass logic pass # Good practice - Use class decorators def add_functionality(cls): # Add functionality return cls @add_functionality class SimpleClass: pass
2. Prefer Descriptors Over getattr
python# Good practice - Use descriptors class ValidatedField: def __get__(self, instance, owner): return instance.__dict__.get(self.name) def __set__(self, instance, value): instance.__dict__[self.name] = value class MyClass: field = ValidatedField() # Bad practice - Use __getattr__ class BadClass: def __getattr__(self, name): return self.__dict__.get(name)
3. Provide Clear Documentation
pythonclass MyMeta(type): """Custom metaclass for adding class-level functionality This metaclass automatically adds a created_at attribute to all classes """ def __new__(cls, name, bases, namespace): namespace['created_at'] = datetime.now() return super().__new__(cls, name, bases, namespace)
4. Consider Performance Impact
python# Cache property access class CachedProperty: def __init__(self, func): self.func = func self.name = func.__name__ def __get__(self, instance, owner): if instance is None: return self if not hasattr(instance, f'_{self.name}'): setattr(instance, f'_{self.name}', self.func(instance)) return getattr(instance, f'_{self.name}') class MyClass: @CachedProperty def expensive_computation(self): # Expensive computation return sum(range(1000000))
Summary
Core concepts of Python metaprogramming:
- Metaclasses: Classes that create classes, controlling the class creation process
- Dynamic Properties: Using
__getattr__,__setattr__etc. to dynamically manage properties - Dynamic Methods: Adding and modifying methods at runtime
- Descriptors: Controlling attribute access and modification
- Property Decorators: Using
@propertyto create computed properties - Dynamic Class Creation: Using
typefunction to dynamically create classes - Class Decorators: Modifying or enhancing class behavior
Metaprogramming use cases:
- Framework development (ORM, form validation)
- Code generation and automation
- Dynamic API creation
- Serialization and deserialization
- Aspect-Oriented Programming
Metaprogramming considerations:
- Use cautiously, avoid over-engineering
- Provide clear documentation and examples
- Consider performance impact
- Prioritize simple solutions
Mastering metaprogramming techniques enables writing more flexible and powerful Python code.