Deep Engineering
Продвинутый·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·25 МИН

Метаклассы: шесть шагов создания класса, из которых видно, что делать метаклассом, а что уже не надо

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

Полное техническое изложение

TL;DR

У класса тоже есть тип, и это type. Объект — экземпляр своего класса, класс — экземпляр своего метакласса; метакласс управляет созданием классов примерно так же, как класс управляет созданием экземпляров. Отдельной сущности в языке для этого нет: чтобы написать свой метакласс, наследуются от type.

Отсюда главное следствие: Class(...) — это вызов экземпляра метакласса, и поэтому создание экземпляров перехватывают у метакласса, а не у класса. А само создание класса — цепочка шагов, и порядок между ними важнее их списка: __set_name__ у дескрипторов и __init_subclass__ у родителя вызываются внутри создания объекта класса, то есть до __init__ метакласса, а декоратор класса вызывается позже всех. Настоящая цена метакласса — тоже не та, которой ждут: не время, а metaclass conflict у того, кто вашего метакласса не выбирал.

Дальше — то, что отличает знающего от читавшего. Шагов шесть:

  1. Meta.__prepare__ — выдаёт словарь под тело
  2. выполняется тело класса
  3. Meta.__new__ — создаётся объект класса
  4. __set_name__ у дескрипторов ← внутри type.__new__
  5. __init_subclass__ у родителя ← внутри type.__new__
  6. Meta.__init__

Шаги 4 и 5 — внутри третьего, то есть до шестого. Метакласс сам по себе не стоит ничего: класс с пустым метаклассом создаётся за 7,09 мкс против 6,98 — разница в пределах шума. Плата берётся ровно за одно — перехват __call__: 223,3 нс на экземпляр против 59,6 (3.13.7). Поэтому с 3.6 (PEP 487) большинство задач решаются __init_subclass__ и __set_name__. Метакласс остаётся нужен, когда надо перехватить само выполнение тела класса или создание экземпляров.

Порог входа
Перед уроком достаточно понимать
  • как объявляют класс и как из него получают экземпляр;
  • что один класс можно унаследовать от другого;
  • что класс в Python — такое же значение, как число или строка: его можно положить в переменную и передать в функцию;
  • что декоратор — это функция, которая получает объект и возвращает объект.
Заранее знать не нужно
  • type как метакласс, __prepare__, type.__new__, __init_subclass__, __set_name__;
  • дескрипторы, порядок разрешения методов, конфликт метаклассов, PEP 487.

База: у класса тоже есть тип

Слово «метакласс» пугает сильнее, чем стоило бы, и почти всегда из-за одного пропущенного шага. Шаг такой: у класса тоже есть тип.

Про объекты это привычно. Написали x = 5 — у x есть тип, и это int. Объявили class Order и создали order = Order() — у order есть тип, и это Order. Вопрос, который обычно не задают: а какой тип у самого Order?

Ответ: type. Класс — такой же объект, как число или строка, и у него тоже есть тип. Тип класса и называется метаклассом.

Дальше модель складывается в две строки, и запоминать надо именно их:

  • объект — экземпляр своего класса;
  • класс — экземпляр своего метакласса.

Отсюда — ответ уровня Junior, который можно произнести на собеседовании целиком: метакласс управляет созданием классов примерно так же, как класс управляет созданием экземпляров. Класс решает, каким получится объект; метакласс решает, каким получится класс.

На этом уровне достаточно. Если на собеседовании спросили «что такое метакласс», ответ выше — верный ответ, а не его упрощённая версия: он ничего не искажает и не обещает сверх того, что есть, и договаривать до конца необходимости нет. Урок помечен как продвинутый не поэтому. Всё дальнейшее — про то, из каких шагов складывается «создание класса», в какой момент можно вмешаться в каждый и почему на практике вмешиваться почти никогда не нужно.

Механизм 1: класс — экземпляр метакласса, и отсюда весь протокол

контракт языкаГарантия языка: класс — объект, а его тип и есть метакласс. Порядок хуков задан справочником, а не реализацией.

Проверяется это одной строкой — и заодно видно, что цепочка на type и кончается:

PYTHON
class Plain:
    pass
 
print(type(Plain))          # <class 'type'>
print(type(type))           # <class 'type'>

Plain — объект. Его тип — type. Метакласс — это просто тип, чьи экземпляры — классы; чтобы написать свой, наследуются от type. Никакой отдельной сущности в языке для метаклассов нет.

Отсюда сразу следует неочевидное: Class(...) — это вызов экземпляра метакласса. Поэтому перехватить создание экземпляров можно, определив __call__ у метакласса, а не у класса. Ровно так делают синглтоны:

PYTHON
class Singleton(type):
    _instances = {}
 
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

Механизм 2: шесть шагов, и порядок между ними важнее их списка

Справочник перечисляет шаги коротко.

When a class definition is executed, the following steps occur: MRO entries are resolved; the appropriate metaclass is determined; the class namespace is prepared; the class body is executed; the class object is created.

перевод

Когда выполняется определение класса, происходят следующие шаги: разрешаются элементы MRO; определяется подходящий метакласс; подготавливается пространство имён класса; выполняется тело класса; создаётся объект класса.

Модель данных, метаклассы

А два самых полезных хука описаны отдельно, в другом разделе, — и именно из-за этого их место в порядке обычно угадывают.

The type.__new__ method collects all of the attributes in the class namespace that define a __set_name__ method; Those __set_name__ methods are called with the class being defined and the assigned name of that particular attribute; The __init_subclass__ hook is called on the immediate parent of the new class in its method resolution order.

перевод

Метод type.__new__ собирает все атрибуты пространства имён класса, определяющие метод __set_name__; эти методы __set_name__ вызываются с определяемым классом и назначенным именем конкретного атрибута; хук __init_subclass__ вызывается у ближайшего родителя нового класса в его порядке разрешения методов.

Модель данных, создание объекта класса

Ключевое здесь — чьё это действие: оба хука вызывает сам type.__new__, то есть они происходят внутри третьего шага, а не после него.

Что из этого следует практически:

  • __init_subclass__ не увидит того, что метакласс делает в __init__ — тот шаг ещё впереди. А метакласс в __init__ уже видит результат обоих хуков.
  • Имя, базы и пространство имён можно менять только до третьего шага. Дальше они перестают быть аргументами и становятся свойствами существующего класса.
  • Декоратор класса вызывается последним, когда всё уже готово: After the class object is created, it is passed to the class decorators included in the class definition (После создания объекта класса он передаётся декораторам класса, указанным в определении класса).

Что «последним» значит для декоратора класса

Из «вызывается последним» легко вывести «поэтому ничего не может», и это неверно. Он получает готовый объект класса — а с готовым классом можно почти всё. Прогон bench/metaclass/decorator_capabilities.py:

__name__ после декоратора                            Переименован
новый атрибут виден                                  добавлено декоратором
декоратор вернул другой класс: имя                   Подменённый
класс на object: __bases__ = (Base,)                 TypeError
класс на Base: __bases__ = (Another,)                Another

Переименовать — может. Дописать и переписать атрибуты — может. Вернуть вместо класса совсем другой объект, и имя в модуле свяжется с ним, — тоже может. С базами сложнее, но запрет не про декоратор: присваивание __bases__ разрешено не всегда (у класса, наследующего прямо от object, оно даёт TypeError), и это правило самого type, а не следствие момента вызова.

Настоящая граница проходит по другому месту — по времени, а не по силе:

до type.__new__готовый класс
__prepare__: отображение, в которое пишется телометакласс
аргументы type.__new__ (имя, базы, пространство имён)метакласс
что увидят __set_name__ и __init_subclass__метакласс
атрибуты, имя, замена объекта целикомметаклассдекоратор

Тот же прогон печатает порядок событий, и в нём видно, что тело класса выполняется в отображении из __prepare__ задолго до декоратора:

__prepare__ дал своё отображение
в пространство имён кладут 'a'
в пространство имён кладут 'b'
Meta.__new__ видит имена: ['a', 'b']
декоратор: тело уже выполнено, класс собран

Механизм 3: что умеет только метакласс

Три умения, и все три следуют из порядка.

Первое — перехватить выполнение тела класса. __prepare__ выдаёт словарь, в который тело будет писаться, и потому видит каждое присваивание отдельно. Ничем другим этого не сделать: к моменту __init_subclass__ тело уже выполнено, и на руках только результат.

PYTHON
class NoDuplicates(dict):
    def __setitem__(self, key, value):
        if key in self:
            raise TypeError(f"имя {key!r} определено в теле класса дважды")
        super().__setitem__(key, value)
 
class StrictMeta(type):
    @classmethod
    def __prepare__(mcls, name, bases, **kwargs):
        return NoDuplicates()

Обычный класс дубль имени пропускает молча — просто побеждает последнее присваивание. С таким метаклассом получается TypeError на этапе определения класса.

Второе — перехватить создание экземпляра через __call__. Это то, из чего делают синглтоны, кеши экземпляров и подмену возвращаемого объекта.

Третье — подменить имя, базы или пространство имён. Всё это аргументы Meta.__new__, то есть третьего шага; к моменту __init_subclass__ и тем более декоратора класс уже создан, и менять их поздно.

Всё остальное, за чем обычно тянутся к метаклассу, — регистрация подклассов, проверка обязательных атрибутов, автоматические имена дескрипторов — закрывается двумя хуками из PEP 487.

Строку про имя, базы и пространство имён в этой таблице стоит читать точно. Речь в ней про них как про аргументы создания класса: подсунуть своё отображение под тело, подменить имя или базы до того, как класс собран, может только метакласс. Готовому классу декоратор имя переставит — это показано выше прогоном. Граница между двумя инструментами проходит по времени, а не по силе.

Механизм 4: граница __init_subclass__

Хук вызывается у родителя, когда создаётся потомок. Справочник сравнивает его с декоратором прямо:

This is closely related to class decorators, but where class decorators only affect the specific class they're applied to, __init_subclass__ solely applies to future subclasses of the class defining the method.

перевод

Это тесно связано с декораторами классов, но если декораторы влияют только на конкретный класс, к которому применены, то __init_subclass__ действует исключительно на будущие подклассы класса, определяющего этот метод.

Модель данных, __init_subclass__

Слово «будущие» — это и есть граница:

PYTHON
class Registry:
    registered = []
 
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Registry.registered.append(cls.__name__)
 
class First(Registry): pass
class Second(Registry): pass
 
print(Registry.registered)      # ['First', 'Second'] — сам Registry не попал

Метакласс, наоборот, срабатывает и на том классе, который его указал: Meta.__new__ вызывается для базового класса так же, как для наследников. Если реестр обязан включать базовый класс — это одна из немногих причин взять метакласс.

И ещё одно: super().__init_subclass__(**kwargs) в первой строке — не формальность. Без него выполнится хук ближайшего родителя, а у всех классов дальше по MRO — нет, и молча: ни ошибки, ни предупреждения не будет.

Глубже: что метакласс стоит на самом деле

наблюдение замераbench/metaclass/cost.py, CPython 3.13.7. Числа сняты на одной машине; содержательно, что плата берётся за перехват __call__, а не за метакласс сам по себе.

Не время. Класс с пустым метаклассом создаётся за 7,09 мкс против 6,98 мкс без него (3.13.7, лучшее из семи прогонов) — разница в пределах шума, и платится она один раз за класс, а не за экземпляр.

Время берётся ровно за одно: перехват __call__ добавляет вызов метода на Python в путь, который иначе идёт целиком в C. 223,3 нс на экземпляр против 59,6 — в 3,7 раза, и на каждом создании. Замер сделан на пустом классе, так что добавка здесь постоянная: у класса, который что-то делает в __init__, та же надбавка весит заметно меньше. Одиночка, сделанный метаклассом, платит эту цену при каждом обращении, включая те, где объект уже создан.

Настоящая же цена — вот эта:

The most derived metaclass is one which is a subtype of all of these candidate metaclasses. If none of the candidate metaclasses meets that criterion, then the class definition will fail with TypeError.

перевод

Наиболее производный метакласс — это тот, который является подтипом всех кандидатов. Если ни один из кандидатов этому критерию не удовлетворяет, определение класса завершится с TypeError.

Модель данных, выбор подходящего метакласса
TypeError: metaclass conflict: the metaclass of a derived class must be
a (non-strict) subclass of the metaclasses of all its bases

Заметьте, где эта ошибка возникает: в чужом файле — у того, кто пишет наследника и вашего метакласса не выбирал. Он всего лишь захотел унаследоваться и от вашего класса, и от чего-то ещё со своим метаклассом — например, от ABC или от Enum. Починить это он может только двумя способами: не наследоваться либо написать третий метакласс, объединяющий два чужих.

Как отвечать на собеседовании

Короткий ответ: метакласс — это тип класса. Класс — экземпляр метакласса ровно так же, как объект — экземпляр класса, и метакласс управляет созданием классов примерно так же, как класс управляет созданием экземпляров. Отсюда и Class(...) — это вызов __call__ у метакласса, потому что класс его экземпляр.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Своими силами метакласс умеет две вещи, которых не умеет больше ничто: перехватить выполнение тела класса через __prepare__ и перехватить создание экземпляра через __call__. Самые частые задачи — регистрация подклассов, проверка их полей, имя дескриптора — с 3.6 решаются __init_subclass__ и __set_name__ дешевле и без конфликта метаклассов. Это совет по проектированию, а не равенство возможностей: подготовка пространства имён, замена баз и перехват создания экземпляра остаются только у метакласса.

Одна формулировка, на которой легко ошибиться вслух, — про декоратор класса. Сказать «декоратор не может заменить класс» неверно: заменить как раз может — он вправе вернуть вместо класса другой объект, и имя в модуле свяжется с ним. Его граница в другом: он не участвует в создании класса. Отображение, в котором выполнялось тело, и аргументы type.__new__ к его приходу уже история, и повлиять на то, что увидят __set_name__ и __init_subclass__, он не может. Разница между ним и метаклассом — по времени вмешательства, а не по силе.

Если спросят «когда вы последний раз писали метакласс» — честный ответ обычно «не писал», и он правильный. Хороший признак того, что вопрос понят: назвать не время как цену, а конфликт метаклассов, и добавить, что платит за него чужой код. И держать границу у замеров: 7,09 против 6,98 мкс и 223,3 против 59,6 нс сняты на одной машине и на пустом классе (3.13.7) — содержательно в них не число, а то, за что именно берётся плата.

Дальше спросят

Спросят дальше

Всё то же самое можно сделать через __init_subclass__?

Короткий ответ

Почти всё, что обычно и хотят, — да, и тогда метакласс не нужен. Граница проходит там, где требуется вмешаться в само создание класса, а не отреагировать на уже созданный наследник: __init_subclass__ вызывается, когда класс уже собран.

Спросят дальше

Метакласс замедляет работу с классом?

Короткий ответ

Сам по себе — нет; платит он на создании класса, а не на обращениях к нему. Замедляет конкретное действие, а не факт наличия метакласса, — и в уроке это разделено замером.

Частые заблуждения

Утверждение

Метаклассы медленные

На самом деле

Создание класса с пустым метаклассом — 7,09 мкс против 6,98 без него, разница в пределах шума, и платится один раз за класс. Время берётся только за перехват __call__: 223,3 нс на экземпляр против 59,6 (3.13.7), то есть в 3,7 раза, и уже на КАЖДОМ создании. Настоящая цена метакласса вообще не во времени, а в конфликте метаклассов у чужого кода.

Утверждение

__init_subclass__ — это просто удобная замена метакласса

На самом деле

Замена в самых частых случаях, но не полная. Он не срабатывает на том классе, где объявлен, — базовый класс в собственный реестр не попадает. Он не участвует в создании класса: пространство имён, в котором выполняется тело, и аргументы type.__new__ к его вызову уже позади (менять атрибуты готового класса он, как и декоратор, может). И он не видит выполнения тела класса: __prepare__ бывает только у метакласса.

Утверждение

__set_name__ и __init_subclass__ вызываются после того, как класс готов

На самом деле

Они вызываются ВНУТРИ type.__new__, то есть между созданием объекта класса и Meta.__init__. Это записано в справочнике и проверяется счётчиком. Отсюда практический ответ: __init_subclass__ не увидит того, что метакласс делает в __init__, — а декоратор класса, наоборот, вызывается позже всех.

Утверждение

Метакласс — это то же самое, что декоратор класса, только сложнее

На самом деле

Декоратор применяется ровно к тому классу, над которым написан: наследник его не получает, и в порядке вызовов для наследника декоратора нет вовсе. Метакласс наследуется. И разница между ними не в силе, а во времени: декоратор получает готовый класс — переименовать, дописать атрибуты и даже вернуть вместо него другой объект он может, — но пространство имён, в котором выполнялось тело, и аргументы type.__new__ к его приходу уже история.

Утверждение

Конфликт метаклассов — редкая экзотика

На самом деле

Он возникает при попытке унаследоваться и от вашего класса, и от чего-то со своим метаклассом — а это ABC и Enum, то есть стандартная библиотека. И возникает он в ЧУЖОМ файле, у автора наследника: сообщение «metaclass conflict» получит он, а не вы.

История версий

ВерсияИзменениеЧто это значит для кода
3.0PEP 3115 вводит __prepare__ и нынешний синтаксис metaclass= вместо атрибута __metaclass__. С этого момента у метакласса появляется единственное умение, которого нет ни у чего другого: перехватить выполнение тела класса.
3.6PEP 487: появляются __init_subclass__ и __set_name__. Это главная строка таблицы — после неё большинство метаклассов писать больше не нужно. Регистрация подклассов, проверка обязательных атрибутов и передача дескриптору его имени решаются без метакласса и без риска конфликта.
3.11Опорная точка урока: порядок шести шагов, поведение __prepare__, граница __init_subclass__ и текст ошибки о конфликте на 3.11 такие же, как на 3.14.7. Проверено запуском одного скрипта на четырёх версиях.
3.12Появляется синтаксис class C[T] (PEP 695) — и метакласс начинает видеть то, чего автор класса не писал. Проверено: __prepare__ получает базы (typing.Generic,) у класса, объявленного без единой базы, а в пространство имён добавляются __type_params__ и __orig_bases__.
3.13В пространство имён, которое получает метакласс, добавляются __firstlineno__ и __static_attributes__. На 3.11 и 3.12 там было ровно __module__, __qualname__ и то, что написано в теле. Метакласс, перебирающий пространство имён и падающий на незнакомом ключе, ломается на этом обновлении — это единственная строка таблицы, способная сломать существующий код.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Базовый класс ведёт реестр наследников через __init_subclass__. Что напечатает этот код?
class Base:
  registry = []

  def __init_subclass__(cls, **kwargs):
      super().__init_subclass__(**kwargs)
      Base.registry.append(cls.__name__)


class A(Base):
  pass


class B(A):
  pass


print(Base.registry)

Практика · оцените

Создание экземпляра обычного класса и класса, у метакласса которого перехвачен __call__. Во сколько раз дороже второе?
раза

Проверьте себя

Вопрос 1 из 5

В каком порядке вызываются __init_subclass__ родителя и Meta.__init__ при создании класса?

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

6 ИСТОЧНИКОВ

  1. Модель данных — создание классаОфициальная документация. Первоисточник порядка шагов. Дословно: «When a class definition is executed, the following steps occur: MRO entries are resolved; the appropriate metaclass is determined; the class namespace is prepared; the class body is executed; the class object is created» (Когда выполняется определение класса, происходят следующие шаги: разрешаются элементы MRO; определяется подходящий метакласс; подготавливается пространство имён класса; выполняется тело класса; создаётся объект класса). Ключевое здесь — что тело выполняется ПОСЛЕ подготовки пространства имён и ДО создания объекта класса.https://docs.python.org/3.14/reference/datamodel.html#metaclasses
  2. Модель данных — что происходит внутри type.__new__Официальная документация. Откуда взято, что __set_name__ и __init_subclass__ вызываются внутри создания класса, а не после него: «The type.__new__ method collects all of the attributes in the class namespace that define a __set_name__ method; Those __set_name__ methods are called with the class being defined and the assigned name of that particular attribute; The __init_subclass__ hook is called on the immediate parent of the new class in its method resolution order» (Метод type.__new__ собирает все атрибуты пространства имён класса, определяющие метод __set_name__; эти методы __set_name__ вызываются с определяемым классом и назначенным именем конкретного атрибута; хук __init_subclass__ вызывается у ближайшего родителя нового класса в его порядке разрешения методов). Там же про декоратор: «After the class object is created, it is passed to the class decorators included in the class definition» (После создания объекта класса он передаётся декораторам класса, указанным в определении класса).https://docs.python.org/3.14/reference/datamodel.html#creating-the-class-object
  3. Модель данных — выбор метакласса и конфликтОфициальная документация. Точная формулировка условия, из-за которого возникает metaclass conflict: «The most derived metaclass is one which is a subtype of all of these candidate metaclasses. If none of the candidate metaclasses meets that criterion, then the class definition will fail with TypeError» (Наиболее производный метакласс — это тот, который является подтипом ВСЕХ кандидатов. Если ни один из кандидатов этому критерию не удовлетворяет, определение класса завершится с TypeError). Отсюда же следует, что ошибка возникает в момент определения класса-наследника.https://docs.python.org/3.14/reference/datamodel.html#determining-the-appropriate-metaclass
  4. Модель данных — __init_subclass__ и его отличие от декоратораОфициальная документация. Прямое сравнение двух инструментов, взятое в урок целиком: «This is closely related to class decorators, but where class decorators only affect the specific class they're applied to, __init_subclass__ solely applies to future subclasses of the class defining the method» (Это тесно связано с декораторами классов, но если декораторы влияют только на конкретный класс, к которому применены, то __init_subclass__ действует исключительно на БУДУЩИЕ подклассы класса, определяющего этот метод). Слово «будущие» и объясняет, почему сам определяющий класс в реестр не попадает.https://docs.python.org/3.14/reference/datamodel.html#object.__init_subclass__
  5. PEP 3115 — Metaclasses in Python 3000PEP. Документ, которым появился __prepare__ — единственный хук, дающий метаклассу то, чего нет ни у одного другого инструмента. Автор Талин, статус Final, Python 3.0. Нужен, чтобы показать: перехват выполнения тела класса — не побочный эффект реализации, а заявленная цель.https://peps.python.org/pep-3115/
  6. PEP 487 — Simpler customisation of class creationPEP. Документ, которым в 3.6 появились __init_subclass__ и __set_name__. Мартин Тайхман и Ник Коглан, Final. Именно он и есть причина, по которой большинство метаклассов сегодня писать не нужно: два хука закрывают самые частые применения, не втягивая класс в конфликт метаклассов.https://peps.python.org/pep-0487/