Лекция 9. Метаклассы и метапрограммирование
1. Введение
В предыдущих лекциях мы изучили классы, наследование, магические методы и дескрипторы. Теперь поднимемся на самый «низкий» и одновременно самый мощный уровень объектной модели Python — туда, где описывается, как интерпретатор создаёт сами классы.
Метапрограммирование — это код, который пишет или изменяет код: программа работает не с обычными данными, а с классами и функциями как с объектами. Понимание этих механизмов отличает разработчика, который использует фреймворк (ORM, валидацию, сериализатор), от того, кто способен такой инструмент построить.
Что мы изучим:
- классы как объекты и
typeкак метакласс по умолчанию; - динамическое создание класса через
type(name, bases, dict); - собственный метакласс:
class Meta(type), методы__new__и__init__; - «лёгкие» альтернативы — хуки
__init_subclass__и__set_name__; - практический пример: мини-ORM с полями-дескрипторами и метаклассом-сборщиком;
- когда метакласс оправдан, а когда избыточен.
Метапрограммирование — острый инструмент. Главное правило практикующего инженера: не применяйте метакласс там, где достаточно дескриптора, декоратора или
__init_subclass__. Эта лекция учит и тому, как пользоваться этими средствами, и тому, когда не нужно.
2. Класс — это объект
Главная идея, без которой метаклассы непостижимы: в Python классы — тоже объекты. У любого объекта есть свой класс (его «тип»). Раз класс — объект, у него тоже есть класс. Класс класса называется метаклассом, и по умолчанию это встроенный type.
class A: pass
a = A()print(type(a)) # <class '__main__.A'> — класс экземпляраprint(type(A)) # <class 'type'> — метакласс классаprint(type(type)) # <class 'type'> — type является метаклассом самому себеПолучается «лестница»: экземпляр → класс → метакласс. type стоит в её вершине и замыкается сам на себя.
Раз класс — объект, его можно класть в переменные, передавать в функции, хранить в списках и создавать во время выполнения программы, а не только инструкцией class.
3. Динамическое создание класса через type
У type две роли. С одним аргументом он возвращает тип объекта (это мы и видели выше). С тремя аргументами он создаёт новый класс:
type(name, bases, namespace)name— строка, имя класса;bases— кортеж базовых классов;namespace— словарь атрибутов и методов будущего класса.
def greet(self): return f"Привет, я {self.name}"
# Полностью эквивалентно инструкции classPerson = type("Person", (object,), {"name": "Аноним", "greet": greet})
p = Person()print(type(p)) # <class '__main__.Person'>print(p.greet()) # Привет, я АнонимЭто и есть ключ к пониманию: обычная инструкция class — лишь синтаксический сахар над вызовом метакласса. Когда вы пишете
class Person: name = "Аноним" def greet(self): ...интерпретатор собирает тело класса в словарь и вызывает type("Person", (object,), {...}). Понимание этого делает следующий шаг — свой метакласс — естественным.
4. Жизненный цикл создания класса
Когда интерпретатор встречает class C(Base, metaclass=Meta): ..., он выполняет такую последовательность:
Meta.__prepare__(name, bases)— возвращает пространство имён (по умолчанию обычныйdict), куда будет собрано тело класса.- Выполняется тело класса — все присваивания и
defпопадают в это пространство имён. Meta.__new__(mcs, name, bases, namespace)— создаёт объект класса.Meta.__init__(cls, name, bases, namespace)— инициализирует уже созданный класс.
Перехватывая любой из этих этапов, мы влияем на то, каким получится класс. Чаще всего достаточно переопределить __new__ или __init__.
5. Собственный метакласс
Метакласс — это класс, унаследованный от type. Чтобы класс создавался через него, указывают параметр metaclass=.
class AutoRepr(type): """Метакласс: добавляет __repr__ всем создаваемым классам."""
def __new__(mcs, name, bases, namespace): cls = super().__new__(mcs, name, bases, namespace)
def __repr__(self): attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items()) return f"{type(self).__name__}({attrs})"
cls.__repr__ = __repr__ return cls
class Point(metaclass=AutoRepr): def __init__(self, x, y): self.x = x self.y = y
print(Point(1, 2)) # Point(x=1, y=2)Обратите внимание на аргументы __new__: первый параметр — сам метакласс (по соглашению mcs, по аналогии с cls), далее имя, базы и словарь тела. Вызов super().__new__(...) создаёт настоящий объект класса, который мы дальше дорабатываем.
Важная особенность: метакласс наследуется. Если у Point появятся подклассы, они тоже будут созданы через AutoRepr — поэтому метакласс удобен для единообразного контроля над целым семейством классов.
5.1. __new__ против __init__
| Метод | Что получает | Когда использовать |
|---|---|---|
__new__ | имя, базы, словарь тела — до создания класса | нужно изменить состав класса: добавить/убрать атрибуты, проверить тело до создания |
__init__ | уже созданный класс cls | достаточно донастроить готовый класс: например, зарегистрировать его где-то |
Правило простое: если меняете будущий класс — работайте в __new__; если только реагируете на уже созданный — хватит __init__.
class Registry(type): classes = {}
def __init__(cls, name, bases, namespace): super().__init__(name, bases, namespace) # класс уже существует — просто регистрируем его Registry.classes[name] = cls
class Service(metaclass=Registry): pass
class Worker(Service): pass
print(Registry.classes) # {'Service': <...>, 'Worker': <...>}6. Лёгкие альтернативы метаклассам
Метаклассы мощны, но тяжелы для чтения и плохо комбинируются (при множественном наследовании метаклассы должны быть совместимы). В Python 3 появились хуки, покрывающие большинство практических задач без написания метакласса.
6.1. __init_subclass__
Этот метод вызывается в базовом классе каждый раз, когда создаётся его подкласс. Он берёт на себя ровно ту роль, ради которой раньше писали метакласс-регистратор.
class Plugin: registry = {}
def __init_subclass__(cls, *, key=None, **kwargs): super().__init_subclass__(**kwargs) name = key or cls.__name__.lower() Plugin.registry[name] = cls
class JsonPlugin(Plugin, key="json"): pass
class XmlPlugin(Plugin): pass
print(Plugin.registry)# {'json': <...JsonPlugin>, 'xmlplugin': <...XmlPlugin>}Два тонких момента. Во-первых, __init_subclass__ неявно является методом класса, его не надо помечать @classmethod. Во-вторых, именованные параметры в скобках класса (key="json") передаются прямо в этот хук — удобный способ настраивать подкласс при объявлении.
6.2. __set_name__
Этот хук относится к дескрипторам, но решает «метапрограммную» задачу: он автоматически сообщает объекту-атрибуту имя, под которым его объявили в классе-владельце. Python вызывает его сам в момент создания класса.
class Field: def __set_name__(self, owner, name): # owner — создаваемый класс, name — имя атрибута self.name = name self.storage = f"__{name}"
class Model: title = Field() price = Field()
print(Model.title.name) # titleprint(Model.price.storage) # __priceБез __set_name__ пришлось бы дублировать имя вручную (title = Field("title")) или собирать имена в метаклассе. Хук избавляет от этого — мощная замена части метаклассовой логики практически бесплатно.
6.3. Правило выбора инструмента
| Задача | Инструмент |
|---|---|
| Контроль одного атрибута (валидация, ленивость) | дескриптор / property |
| Узнать имя атрибута внутри дескриптора | __set_name__ |
| Реакция на создание подклассов (реестр, проверка) | __init_subclass__ |
| Донастройка готового класса | декоратор класса |
| Тотальный контроль создания класса, DSL, проверка тела | метакласс |
Эмпирическое правило (известное как «правило Тима Питерса»): если вы не уверены, нужен ли метакласс, — он вам не нужен.
7. Практический пример: мини-ORM
Соберём всё вместе. Построим декларативную модель, как в настоящих ORM (Django, SQLAlchemy): пользователь лишь описывает поля, а вся «магия» спрятана внутри. Используем два механизма:
- дескрипторы — для типизированных полей с валидацией (поведение отдельного поля);
- метакласс — для сбора всех полей в общий реестр (поведение класса в целом).
from typing import Any
# ---------- Поля-дескрипторы ----------
class Field: """Базовое типизированное поле."""
def __init__(self, *, required: bool = True, default: Any = None): self.required = required self.default = default
def __set_name__(self, owner, name): self.name = name self.storage = f"__{name}"
def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self.storage, self.default)
def __set__(self, instance, value): value = self.validate(value) setattr(instance, self.storage, value)
def validate(self, value): if value is None and self.required: raise ValueError(f"Поле {self.name!r} обязательно") return value
class IntegerField(Field): def __init__(self, *, min_value=None, **kwargs): super().__init__(**kwargs) self.min_value = min_value
def validate(self, value): value = super().validate(value) if value is not None: if not isinstance(value, int): raise TypeError(f"{self.name!r} должно быть int") if self.min_value is not None and value < self.min_value: raise ValueError(f"{self.name!r} >= {self.min_value}") return value
class StringField(Field): def __init__(self, *, max_length=None, **kwargs): super().__init__(**kwargs) self.max_length = max_length
def validate(self, value): value = super().validate(value) if value is not None: if not isinstance(value, str): raise TypeError(f"{self.name!r} должно быть str") if self.max_length is not None and len(value) > self.max_length: raise ValueError(f"{self.name!r}: максимум {self.max_length} символов") return value
# ---------- Метакласс модели ----------
class ModelMeta(type): """Собирает все Field класса (и родителей) в реестр _fields."""
def __new__(mcs, name, bases, namespace): fields = { key: value for key, value in namespace.items() if isinstance(value, Field) } for base in bases: # наследуем поля родителей fields.update(getattr(base, "_fields", {}))
cls = super().__new__(mcs, name, bases, namespace) cls._fields = fields return cls
class Model(metaclass=ModelMeta): def __init__(self, **kwargs): for field_name, field in self._fields.items(): value = kwargs.get(field_name, field.default) setattr(self, field_name, value) # пройдёт через дескриптор -> валидация
def to_dict(self): return {name: getattr(self, name) for name in self._fields}
def __repr__(self): body = ", ".join(f"{k}={v!r}" for k, v in self.to_dict().items()) return f"{type(self).__name__}({body})"
# ---------- Использование ----------
class User(Model): id = IntegerField(min_value=1) name = StringField(max_length=50) age = IntegerField(required=False, default=0, min_value=0)
u = User(id=1, name="Алиса", age=30)print(u) # User(id=1, name='Алиса', age=30)print(u.to_dict()) # {'id': 1, 'name': 'Алиса', 'age': 30}
try: User(id=0, name="Боб") # id < min_valueexcept ValueError as e: print("Ошибка:", e)
try: User(id=2, name=12345) # name не strexcept TypeError as e: print("Ошибка:", e)Этот пример показывает чёткое разделение ответственности:
- дескрипторы (
Fieldи наследники) отвечают за поведение отдельного поля — хранение и валидацию, причём__set_name__сам связывает поле с его именем; - метакласс
ModelMetaотвечает за класс в целом — собирает поля в_fields, в том числе унаследованные; - пользователь пишет только декларацию — лаконично и безопасно, не зная о внутренней «магии».
Заметьте: эту конкретную задачу (сбор полей) можно решить и через
__init_subclass__в самомModel, без отдельного метакласса. Метакласс здесь — учебная демонстрация полного цикла; в реальном коде стоит выбрать более лёгкий инструмент, если он справляется.
8. Когда метакласс оправдан, а когда избыточен
Метакласс уместен, когда нужно единообразно контролировать создание целого семейства классов, и более лёгких средств не хватает: проверка контракта на этапе объявления (так устроен abc.ABCMeta), построение DSL и декларативных API, внедрение общего поведения во всю иерархию до её использования.
Метакласс избыточен, когда задачу решает что-то проще: контроль одного атрибута — дескриптор или @property; реакция на появление подкласса — __init_subclass__; донастройка одного готового класса — декоратор класса; знание имени атрибута внутри дескриптора — __set_name__.
Цена метакласса — читаемость и совместимость: код труднее понять, а при множественном наследовании метаклассы должны быть согласованы, иначе возникнет конфликт. Поэтому действует принцип наименее мощного инструмента: берите самое простое средство, которое решает задачу.
Краткие итоги
- Классы — это объекты. У класса есть свой класс — метакласс, по умолчанию
type. type(name, bases, namespace)создаёт класс динамически; инструкцияclass— синтаксический сахар над этим вызовом.- Создание класса проходит этапы
__prepare__→ выполнение тела →__new__→__init__; перехватывая их, мы влияем на класс. - Свой метакласс наследуется от
type.__new__меняет состав будущего класса,__init__донастраивает уже созданный. Подключается черезmetaclass=. __init_subclass__и__set_name__— лёгкие хуки, заменяющие метакласс в большинстве задач (реестры, связывание полей с именами).- Хорошая абстракция (как мини-ORM) прячет метапрограммирование за декларативным API: сложность внутри инструмента, простота снаружи.
- Главный принцип: выбирайте наименее мощный инструмент. Дескриптор лучше метакласса,
__init_subclass__лучше метакласса, явный код лучше «магии».
Вопросы для самопроверки
- Что выведет
type(type)и почему? Что такое метакласс? - Создайте класс
Animalс атрибутомlegs = 4и методомspeak, используя только вызовtype(...), без инструкцииclass. - В каком порядке вызываются
__prepare__,__new__,__init__метакласса и выполнение тела класса? - Чем отличается
__new__метакласса от__init__? В каком случае нужен именно__new__? - Почему параметр
metaclass=наследуется подклассами? Как это используют для контроля семейства классов? - Какую задачу решает
__init_subclass__? Перепишите через него метакласс-регистратор из раздела 5.1. - Зачем нужен
__set_name__и что было бы без него в примере с полямиField? - В мини-ORM из раздела 7: можно ли убрать метакласс
ModelMetaи собрать_fieldsчерез__init_subclass__? Набросайте такой вариант. - Приведите две задачи, где метакласс оправдан, и две, где он избыточен. Чем заменить его во втором случае?
- Сформулируйте «правило Тима Питерса» и объясните, почему оно полезно на практике.