Метаклассы в Python: когда оправданы и чем заменяются
Метаклассы решают ровно одну задачу — вмешаться в создание класса. В 90% случаев её закрывают __init_subclass__ и декораторы. Разбираем, где метакласс оправдан, а где это оверинжиниринг.

Метакласс переопределяют, чтобы отловить рефакторинг, поймать баг в наследнике или упростить регистрацию плагинов — и в половине случаев через год код становится нечитаемым, потому что задачу можно было решить без метаклассов. С Python 3.6 в языке появился хук __init_subclass__, а декораторы классов существуют ещё дольше. Оба закрывают большинство сценариев, ради которых раньше писали метакласс. Разберём, где метакласс действительно единственный инструмент, а где он — усложнение ради усложнения.
Материал для Python 3.10+. Всё, что касается __init_subclass__ и __set_name__, работает начиная с 3.6; typing-примеры используют синтаксис, доступный в 3.10.
Что вообще делает метакласс
Класс в Python — это объект. Его создаёт другой класс, по умолчанию type. Когда интерпретатор доходит до class Foo: ..., он собирает тело класса в словарь и вызывает type('Foo', bases, namespace). Метакласс — это то, что подставляется вместо type.
Отсюда единственная реальная возможность метакласса: вмешаться в момент создания класса — до того, как класс станет доступен остальному коду. Не экземпляра, а самого класса. Всё остальное, что приписывают метаклассам, — следствие этого или делается проще другими средствами.
# Python 3.10
class UpperAttrMeta(type):
def __new__(mcs, name, bases, namespace):
# приводим имена всех методов-не-дандеров к верхнему регистру
renamed = {}
for key, value in namespace.items():
if not key.startswith('__'):
renamed[key.upper()] = value
else:
renamed[key] = value
return super().__new__(mcs, name, bases, renamed)
class Service(metaclass=UpperAttrMeta):
def ping(self):
return 'pong'
s = Service()
print(s.PING()) # pong — метод переименован при создании класса
Пример искусственный, но показывает точку входа: __new__ метакласса получает пространство имён будущего класса и может его переписать. Ровно этот перехват и есть причина, по которой метаклассы существуют.
Три задачи, которые раньше решали метаклассом
До 3.6 метакласс был единственным способом сделать три вещи. Сегодня у каждой есть более лёгкая замена.
1. Реакция на создание подкласса
Классический случай — реестр плагинов: каждый наследник базового класса должен автоматически регистрироваться. Раньше это писали в __init__ метакласса. Теперь есть __init_subclass__ — метод-хук на самом базовом классе, который вызывается при определении каждого наследника.
# Python 3.10
class Handler:
registry = {}
def __init_subclass__(cls, *, route, **kwargs):
super().__init_subclass__(**kwargs)
# cls — только что созданный подкласс
Handler.registry[route] = cls
class JsonHandler(Handler, route='/json'):
pass
class HtmlHandler(Handler, route='/html'):
pass
print(Handler.registry)
# {'/json': , '/html': }
Обратите внимание: route='/json' передаётся прямо в объявление класса и приходит в __init_subclass__ как именованный аргумент. Метакласс для такого больше не нужен, а читается это на порядок проще.
2. Реакция на присвоение атрибута дескриптору
ORM и валидаторы полей часто хотят знать, под каким именем поле объявлено в классе. Раньше имя выцарапывали из пространства имён в метаклассе. С 3.6 есть __set_name__ — дескриптор сам узнаёт своё имя.
# Python 3.10
class Field:
def __set_name__(self, owner, name):
# вызывается при создании класса-владельца
self.name = name
self.private = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
setattr(obj, self.private, value)
class User:
login = Field()
email = Field()
u = User()
u.login = 'ivan'
print(User.login.name) # 'login' — поле само узнало своё имя
3. Модификация тела класса целиком
Добавить методы, обернуть все существующие, изменить __slots__ — это делает декоратор класса. Он получает уже готовый класс и возвращает его же (или подменённый).
# Python 3.10
import functools
def add_repr(cls):
# генерируем __repr__ по аннотациям класса
fields = list(getattr(cls, '__annotations__', {}))
def __repr__(self):
parts = ', '.join(f'{f}={getattr(self, f)!r}' for f in fields)
return f'{cls.__name__}({parts})'
cls.__repr__ = __repr__
return cls
@add_repr
class Point:
x: int
y: int
def __init__(self, x, y):
self.x, self.y = x, y
print(repr(Point(1, 2))) # Point(x=1, y=2)
Именно так работает @dataclass из стандартной библиотеки — это декоратор класса, а не метакласс.
Сравнение инструментов
| Инструмент | Что перехватывает | Когда применять | Сложность чтения |
|---|---|---|---|
__init_subclass__ | Создание подкласса | Регистрация, проверка обязательных атрибутов у наследников | Низкая |
__set_name__ | Присвоение имени дескриптору | Поля ORM, валидаторы, дескрипторы, которым нужно своё имя | Низкая |
| Декоратор класса | Готовый класс целиком | Генерация методов, обёртки, инъекция атрибутов | Средняя |
| Метакласс | Само создание класса | Управление bases, кастомный namespace, контроль isinstance | Высокая |
Когда метакласс всё-таки оправдан
Остаётся узкий набор задач, где хуки и декораторы не работают, потому что вмешаться нужно раньше, чем класс вообще собран.
- Управление порядком тела класса. Метакласс может вернуть из
__prepare__не обычныйdict, а специальный маппинг — например, чтобы запретить дублирование имён в теле или зафиксировать порядок объявления там, где обычного словаря недостаточно.__init_subclass__и декораторы получают уже собранныйnamespaceи на его формирование повлиять не могут. - Переопределение
isinstanceиissubclass. Через__instancecheck__и__subclasscheck__на метаклассе. Так устроены абстрактные базовые классы изabc:ABCMeta— это метакласс. - Изменение
basesдо создания класса. Вставить примесь во все наследники, переписать MRO. Декоратор уже опоздал — класс создан с прежними базами. - Интеграция с C-уровнем и фреймворками, которые сами построены на метаклассах. Django Models, SQLAlchemy declarative, Enum, Protocol из
typing— все используют метаклассы, и при написании расширений к ним вы имеете дело с их метаклассами хотите того или нет.
Если задачу можно решить хуком или декоратором — решайте им. Метакласс оправдан, только когда нужно вмешаться в само создание класса, а не в его результат. Всё остальное — усложнение, за которое расплачивается тот, кто будет читать код после вас.
Что ломается при использовании метаклассов
Три проблемы всплывают в проде чаще остального.
- Конфликт метаклассов при множественном наследовании. Если два базовых класса используют разные, несовместимые метаклассы, Python бросит
TypeError: metaclass conflict. Это ловится не при определении базовых классов, а при попытке их объединить в наследнике — часто уже в чужом коде. - Порядок вызовов неочевиден.
__prepare__→ тело класса →__new__метакласса →__init__метакласса →__set_name__дескрипторов →__init_subclass__базового класса. Ошибка в порядке приводит к тому, что атрибут ещё не готов, когда его читают. - Инструменты статического анализа хуже понимают метаклассы. mypy и IDE выводят типы для
__init_subclass__и декораторов уверенно, а динамически сгенерированные метаклассом атрибуты часто теряются — автодополнение не работает, проверка типов молчит.
Как выбрать в конкретной ситуации
- Нужно среагировать на появление наследника (регистрация, проверка контракта)? —
__init_subclass__. - Полю или дескриптору нужно знать своё имя в классе? —
__set_name__. - Нужно сгенерировать или обернуть методы готового класса? — декоратор класса.
- Нужно вмешаться в
bases,__prepare__илиisinstance? — только тогда метакласс. - Пишете расширение к Django/SQLAlchemy/Enum? — используете их метакласс, свой не изобретаете.
Что делать дальше
Пройдитесь по своему коду и найдите объявления metaclass=. Для каждого проверьте по списку выше: если задача — регистрация наследников или именование полей, перепишите на __init_subclass__ и __set_name__ и замерьте, насколько сократился и упростился код. Для понимания механики создания класса прочитайте главу Data Model в официальной документации — разделы про __init_subclass__, __set_name__ и __prepare__. В проде следите за одним показателем: если статический анализатор перестал видеть атрибуты класса, метакласс скорее всего лишний.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Python Data model — Customizing class creation, PEP 487 — Simpler customisation of class creation
Частые вопросы
Чем метакласс отличается от декоратора класса на практике?
У меня Python 3.5, там нет __init_subclass__ и __set_name__. Что делать?
Почему я получаю TypeError: metaclass conflict?
dataclass — это метакласс?
Метаклассы замедляют программу?
Можно ли использовать __init_subclass__ и метакласс одновременно?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.