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

Лекция 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}"
# Полностью эквивалентно инструкции class
Person = 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): ..., он выполняет такую последовательность:

  1. Meta.__prepare__(name, bases) — возвращает пространство имён (по умолчанию обычный dict), куда будет собрано тело класса.
  2. Выполняется тело класса — все присваивания и def попадают в это пространство имён.
  3. Meta.__new__(mcs, name, bases, namespace)создаёт объект класса.
  4. 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) # title
print(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_value
except ValueError as e:
print("Ошибка:", e)
try:
User(id=2, name=12345) # name не str
except TypeError as e:
print("Ошибка:", e)

Этот пример показывает чёткое разделение ответственности:

  • дескрипторы (Field и наследники) отвечают за поведение отдельного поля — хранение и валидацию, причём __set_name__ сам связывает поле с его именем;
  • метакласс ModelMeta отвечает за класс в целом — собирает поля в _fields, в том числе унаследованные;
  • пользователь пишет только декларацию — лаконично и безопасно, не зная о внутренней «магии».

Заметьте: эту конкретную задачу (сбор полей) можно решить и через __init_subclass__ в самом Model, без отдельного метакласса. Метакласс здесь — учебная демонстрация полного цикла; в реальном коде стоит выбрать более лёгкий инструмент, если он справляется.


8. Когда метакласс оправдан, а когда избыточен

Метакласс уместен, когда нужно единообразно контролировать создание целого семейства классов, и более лёгких средств не хватает: проверка контракта на этапе объявления (так устроен abc.ABCMeta), построение DSL и декларативных API, внедрение общего поведения во всю иерархию до её использования.

Метакласс избыточен, когда задачу решает что-то проще: контроль одного атрибута — дескриптор или @property; реакция на появление подкласса — __init_subclass__; донастройка одного готового класса — декоратор класса; знание имени атрибута внутри дескриптора — __set_name__.

Цена метакласса — читаемость и совместимость: код труднее понять, а при множественном наследовании метаклассы должны быть согласованы, иначе возникнет конфликт. Поэтому действует принцип наименее мощного инструмента: берите самое простое средство, которое решает задачу.


Краткие итоги

  1. Классы — это объекты. У класса есть свой класс — метакласс, по умолчанию type.
  2. type(name, bases, namespace) создаёт класс динамически; инструкция class — синтаксический сахар над этим вызовом.
  3. Создание класса проходит этапы __prepare__ → выполнение тела → __new____init__; перехватывая их, мы влияем на класс.
  4. Свой метакласс наследуется от type. __new__ меняет состав будущего класса, __init__ донастраивает уже созданный. Подключается через metaclass=.
  5. __init_subclass__ и __set_name__ — лёгкие хуки, заменяющие метакласс в большинстве задач (реестры, связывание полей с именами).
  6. Хорошая абстракция (как мини-ORM) прячет метапрограммирование за декларативным API: сложность внутри инструмента, простота снаружи.
  7. Главный принцип: выбирайте наименее мощный инструмент. Дескриптор лучше метакласса, __init_subclass__ лучше метакласса, явный код лучше «магии».

Вопросы для самопроверки

  1. Что выведет type(type) и почему? Что такое метакласс?
  2. Создайте класс Animal с атрибутом legs = 4 и методом speak, используя только вызов type(...), без инструкции class.
  3. В каком порядке вызываются __prepare__, __new__, __init__ метакласса и выполнение тела класса?
  4. Чем отличается __new__ метакласса от __init__? В каком случае нужен именно __new__?
  5. Почему параметр metaclass= наследуется подклассами? Как это используют для контроля семейства классов?
  6. Какую задачу решает __init_subclass__? Перепишите через него метакласс-регистратор из раздела 5.1.
  7. Зачем нужен __set_name__ и что было бы без него в примере с полями Field?
  8. В мини-ORM из раздела 7: можно ли убрать метакласс ModelMeta и собрать _fields через __init_subclass__? Набросайте такой вариант.
  9. Приведите две задачи, где метакласс оправдан, и две, где он избыточен. Чем заменить его во втором случае?
  10. Сформулируйте «правило Тима Питерса» и объясните, почему оно полезно на практике.