Перейти к основному содержимому

Метапрограммирование

📺 Слайды к лекции

Материалы в разработке

Эта страница находится в процессе подготовки и не является финальной версией. Содержание будет дорабатываться и обновляться в течение текущего семестра.

type() — создание классов динамически

В Python type() может не только возвращать тип объекта, но и создавать новые классы динамически:

type_create.py
# Обычное определение класса
class Dog:
sound = "Woof"
def speak(self):
return self.sound

# Эквивалент через type()
Dog = type('Dog', (object,), {
'sound': 'Woof',
'speak': lambda self: self.sound,
})

d = Dog()
print(d.speak()) # Woof
Определение

type(name, bases, dict) — конструктор классов. name — имя класса, bases — кортеж базовых классов, dict — словарь атрибутов и методов. Любой класс Python создаётся именно этим механизмом.

Метаклассы

Метакласс — это «класс класса». По умолчанию метакласс всех классов — type.

metaclass.py
class Meta(type):
def __new__(mcs, name, bases, namespace):
print(f"Creating class: {name}")
# Можно модифицировать namespace перед созданием класса
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):
x = 42

# Creating class: MyClass
# Initializing class: MyClass

__call__ в метаклассе

Метакласс может контролировать создание экземпляров через __call__:

singleton.py
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 Database(metaclass=SingletonMeta):
def __init__(self):
self.connection = "connected"

db1 = Database()
db2 = Database()
print(db1 is db2) # True — один и тот же экземпляр

__init_subclass__ — упрощённая альтернатива

Python 3.6 добавил __init_subclass__ — хук, вызываемый при создании подкласса. Часто достаточен вместо метакласса:

init_subclass.py
class Plugin:
_registry = {}

def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__.lower()
Plugin._registry[name] = cls
print(f"Registered plugin: {name}")

class JSONPlugin(Plugin, plugin_name="json"):
pass

class XMLPlugin(Plugin, plugin_name="xml"):
pass

print(Plugin._registry)
# {'json': <class 'JSONPlugin'>, 'xml': <class 'XMLPlugin'>}
Практический совет

В 90% случаев __init_subclass__ достаточно вместо метаклассов. Метаклассы нужны, только когда требуется изменить сам процесс создания класса (модификация namespace, контроль __call__).

Декораторы классов

Декоратор класса — функция, принимающая класс и возвращающая модифицированный класс:

class_decorator.py
def add_repr(cls):
def __repr__(self):
attrs = ', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())
return f'{cls.__name__}({attrs})'
cls.__repr__ = __repr__
return cls

@add_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

print(Point(1, 2)) # Point(x=1, y=2)

__set_name__ — протокол именования дескрипторов

Python 3.6 вызывает __set_name__ на дескрипторе при создании класса-владельца:

set_name.py
class TypeChecked:
def __set_name__(self, owner, name):
self.name = name # автоматически получает имя атрибута

def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError(f"{self.name} must be int, got {type(value)}")
instance.__dict__[self.name] = value

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)

class Config:
max_retries = TypeChecked()
timeout = TypeChecked()

c = Config()
c.max_retries = 3 # OK
# c.timeout = "fast" # TypeError: timeout must be int

__class_getitem__ — параметризованные типы

class_getitem.py
class Matrix:
def __class_getitem__(cls, params):
rows, cols = params
print(f"Matrix[{rows}, {cols}]")
return cls

# Использование в аннотациях типов
def process(m: Matrix[3, 3]) -> Matrix[3, 1]:
...

Абстрактные метаклассы (ABCMeta)

ABCMeta — метакласс для создания абстрактных базовых классов:

abcmeta.py
from abc import ABCMeta, abstractmethod

class Serializable(metaclass=ABCMeta):
@abstractmethod
def serialize(self) -> bytes:
...

@abstractmethod
def deserialize(self, data: bytes):
...

# Serializable() # TypeError — нельзя инстанцировать

Практический пример: ORM-style синтаксис

Метаклассы позволяют создать декларативный синтаксис в стиле Django ORM:

orm_meta.py
class Field:
def __init__(self, field_type):
self.field_type = field_type

def __set_name__(self, owner, name):
self.name = name

def __set__(self, instance, value):
if not isinstance(value, self.field_type):
raise TypeError(
f"{self.name}: expected {self.field_type.__name__}, "
f"got {type(value).__name__}"
)
instance.__dict__[self.name] = value

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)

class ModelMeta(type):
def __new__(mcs, name, bases, namespace):
fields = {
k: v for k, v in namespace.items()
if isinstance(v, Field)
}
cls = super().__new__(mcs, name, bases, namespace)
cls._fields = fields
return cls

class Model(metaclass=ModelMeta):
_fields: dict = {}

def __repr__(self):
attrs = ', '.join(
f'{name}={getattr(self, name, None)!r}'
for name in self._fields
)
return f'{type(self).__name__}({attrs})'

class User(Model):
name = Field(str)
age = Field(int)

u = User()
u.name = "Alice"
u.age = 30
print(u) # User(name='Alice', age=30)

Когда использовать (и когда НЕ использовать) метаклассы

Частая ошибка

Метаклассы — мощный, но сложный инструмент. Не используйте их без необходимости.

НужноИспользуйте
Валидация при создании подклассов__init_subclass__
Авто-регистрация плагинов__init_subclass__
Добавление методов к классуДекоратор класса
Генерация __repr__, __eq__@dataclass
Контроль создания экземпляров (Singleton)Метакласс (__call__)
ORM-style декларативный синтаксисМетакласс (__new__)
Модификация namespace до создания классаМетакласс (__prepare__)