PythonSeniorTechnical
Что такое метакласс и когда он реально нужен?
Метакласс — класс, который создаёт классы (по умолчанию type). Через __prepare__, __new__, __init__, __call__ можно вмешаться в создание класса: автоматическая регистрация, валидация полей, ABC, ORM declarative base. В 90% случаев хватит __init_subclass__ или class-decorator.
Что это
В Python всё, включая классы, — объекты. Класс класса (то, что создаёт класс) называется метаклассом. По умолчанию это type. class Foo: ... эквивалентно Foo = type("Foo", (), {...}).
Пользовательский метакласс задаётся через class Foo(metaclass=Meta):. Meta наследуется от type и переопределяет хуки создания класса.
Этапы создания класса
- Выбор метакласса: явно через
metaclass=, иначе — общий предок метаклассов всех баз. Meta.__prepare__(name, bases, **kwds)— возвращает namespace (по умолчаниюdict). Можно вернутьOrderedDict/специальный mapping, чтобы перехватить присваивания в теле класса.- Тело класса исполняется в этом namespace.
Meta.__new__(mcls, name, bases, namespace, **kwds)— создаёт объект класса.Meta.__init__(cls, name, bases, namespace, **kwds)— пост-инициализация.__set_name__у дескрипторов,__init_subclass__у базы — вызываются на этапе создания.
Когда метакласс реально нужен
- Framework declarative bases: SQLAlchemy
DeclarativeMeta, DjangoModelBase, Pydantic v1ModelMetaclass— собирают поля, строят mapper, инсталлируют дескрипторы. - ABCs:
abc.ABCMetaрегистрирует виртуальные подклассы черезregister(), проверяет abstract-методы. - Перехват синтаксиса класса: например, разрешить дублирующиеся имена и собирать их в список (через
__prepare__с особым dict). - Плагинная регистрация всех наследников в registry.
- Type-level контроль: запретить наследование, добавить обязательные атрибуты, форсировать namespace.
Почти всегда достаточно более простых средств
__init_subclass__(PEP 487, 3.6+) — хук на создание подкласса, без метакласса. Покрывает 80% сценариев регистрации.- Class decorator — функция, которая принимает класс и возвращает класс (или модифицирует). Чище читается, проще тестируется.
__set_name__у дескрипторов — для привязки к имени атрибута без метакласса.
Пример: registry через метакласс
class RegistryMeta(type):
registry: dict[str, type] = {}
def __new__(mcls, name, bases, namespace, **kw):
cls = super().__new__(mcls, name, bases, namespace)
if bases: # пропускаем сам корневой Plugin
mcls.registry[name] = cls
return cls
class Plugin(metaclass=RegistryMeta):
pass
class CsvPlugin(Plugin):
fmt = "csv"
class JsonPlugin(Plugin):
fmt = "json"
print(RegistryMeta.registry)
# {'CsvPlugin': <class CsvPlugin>, 'JsonPlugin': <class JsonPlugin>}
То же через __init_subclass__ (проще)
class Plugin:
registry: dict[str, type] = {}
def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
Plugin.registry[cls.__name__] = cls
class CsvPlugin(Plugin):
fmt = "csv"
print(Plugin.registry) # {'CsvPlugin': <class CsvPlugin>}
Подводные камни
- Конфликт метаклассов при множественном наследовании:
metaclass conflict— все базы должны иметь совместимый метакласс. Решается общим mcls-наследником. - Метакласс делает код менее читаемым; junior-ы не понимают, откуда «магия». Документируйте.
- Тестирование классов с метаклассами сложнее — побочные эффекты на этапе импорта.
- Перепутать
type(obj)иtypeкак метакласс — две разные вещи. - Использовать метакласс там, где достаточно
__init_subclass__или class-decorator — over-engineering. - Pydantic, attrs, dataclass в современном виде НЕ требуют метакласс (или используют его легко); раньше pydantic v1 имел Metaclass, в v2 ушёл на
__init_subclass__и более тонкие хуки.
Common mistakes
- Говорить, что метакласс нужен для любого factory.
- Не знать, что type — метакласс обычных классов.
- Не упоминать альтернативы.
What the interviewer is testing
- Объясняет classes are objects.
- Понимает этапы создания класса.
- Называет реальные framework-level use cases.