MEASUREMENT
bench/metaclass/order.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/python/metaclasses
- How to run it
python3.11 bench/metaclass/order.py python3.12 bench/metaclass/order.py python3.13 bench/metaclass/order.py python3.14 bench/metaclass/order.py python3.13 bench/metaclass/cost.py python3.14 bench/metaclass/cost.py
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Метаклассы»
| Скрипт | Что делает |
|---|---|
order.py |
наблюдения без времени: порядок шести шагов создания класса, что такое метакласс, __prepare__, граница __init_subclass__, конфликт метаклассов |
cost.py |
цена: создание класса с метаклассом и без, создание экземпляра с перехваченным __call__ и без |
python3.11 bench/metaclass/order.py
python3.12 bench/metaclass/order.py
python3.13 bench/metaclass/order.py
python3.14 bench/metaclass/order.py
python3.13 bench/metaclass/cost.py
python3.14 bench/metaclass/cost.py
Практика урока
practice.py — источник ответов двух практических задач урока, а runs/practice.txt —
дословная запись его прогона. Ответ задачи не сочиняется: сборка сверяет
заявленное с этой записью (scripts/validate-practice.mjs) и не проходит,
если они разошлись.
Прогон снят 30.08.2026 на CPython 3.13.7 (Clang 20.1.4). Абсолютные числа — этой машины; переносится кратность, и задача «во сколько раз» стоит именно на ней.
Кратность устойчива только потому, что формы меряются ВПЕРЕМЕЖКУ: в каждом круге меряются все, минимум для каждой берётся по кругам. Пока замеры шли подряд, просадка машины в окне одной формы целиком доставалась ей, и отношение гуляло в полтора раза от запуска к запуску (измерено на декораторах: 5,9 / 7,1 / 7,5 / 9,2). После перехода на чередование расхождение между прогонами не выходит за несколько процентов. Перезаписывать запись прогона имеет смысл только вместе с проверкой задачи: если после перезапуска ответ изменился, менять нужно задачу, а не файл.
Что здесь важно прочитать правильно
order.py не меряет время — он меряет порядок. Каждый хук дописывает
строку в общий список, и печатается список. Такое наблюдение от машины не
зависит и обязано совпасть у любого, кто запустит скрипт на той же версии.
Порядок — главный результат обоих скриптов. В документации он рассыпан по
трём разделам: __prepare__ описан в модели данных, __set_name__ — в
разделе про дескрипторы, __init_subclass__ — в разделе про классы. Пока они
не выстроены в одну колонку, на вопросы вида «увидит ли __init_subclass__
то, что сделал метакласс» ответить нечем.
cost.py уточняет утверждение «метаклассы дорогие», и уточнение важнее
самого утверждения. Создание класса с метаклассом стоит столько же, сколько
без него. Платится за метакласс в другом месте: перехват __call__ добавляет
вызов метода на Python на КАЖДОМ создании экземпляра.
Что получилось на 3.13.7, одинаково на 3.11, 3.12, 3.13 и 3.14.7:
Создание класса — шесть шагов:
Meta.__prepare__— выдаёт словарь под тело класса- тело класса выполняется
Meta.__new__— создаёт объект класса__set_name__у дескрипторов__init_subclass__у родителяMeta.__init__— дописывает уже созданный класс
Создание экземпляра — три:
Meta.__call__Class.__new__Class.__init__
Шаги 4 и 5 происходят ВНУТРИ type.__new__, то есть до Meta.__init__:
__init_subclass__ не видит того, что метакласс делает в __init__, а
метакласс в __init__ видит результат обоих.
Цена (3.13.7):
| что | сколько |
|---|---|
| создание класса без метакласса | 6,98 мкс |
| создание класса с пустым метаклассом | 7,09 мкс |
создание класса с __init_subclass__ |
6,88 мкс |
| создание экземпляра, обычный класс | 59,6 нс |
создание экземпляра, метакласс перехватил __call__ |
223,3 нс — ×3,7 |
От запуска к запуску отношение по экземплярам держится в пределах 3,7–3,8.
Что этими числами доказать НЕЛЬЗЯ
Что метаклассы «бесплатны». Их цена реальна, просто она не во времени: конфликт
метаклассов (metaclass conflict) возникает у того, кто вашего метакласса не
выбирал, — у автора класса, который наследуется и от вашего, и от чего-то ещё
со своим метаклассом, например от ABC или Enum. Ошибка при этом появляется
в чужом файле.
Script
292 lines"""
Наблюдения для урока «Метаклассы».
ЗАПУСК (порядок урока снят на 3.13.7 и совпал на всех четырёх версиях):
for v in 3.11 3.12 3.13 3.14; do python$v bench/metaclass/order.py; done
ЗДЕСЬ НЕТ ЗАМЕРОВ ВРЕМЕНИ. Скрипт печатает ПОРЯДОК вызовов — то, что можно
записать счётчиком, а не секундомером. Время меряется отдельно, в `cost.py`.
ГЛАВНОЕ, РАДИ ЧЕГО ОН НАПИСАН. Порядок шести шагов создания класса нигде не
собран в одном месте: `__prepare__` описан в модели данных, `__set_name__` —
в разделе про дескрипторы, `__init_subclass__` — в разделе про классы. Пока
они не выстроены в одну колонку, невозможно ответить на вопросы вида «увидит
ли __init_subclass__ то, что сделал __new__ метакласса» — а ответ на них
следует из порядка и больше ниоткуда.
"""
import sys
LOG: list[str] = []
def header(title: str) -> None:
print(f"\n--- {title} ---")
def show(title: str) -> None:
print(f"{title}:")
for number, step in enumerate(LOG, 1):
print(f" {number}. {step}")
LOG.clear()
def observe_order() -> None:
"""Семь шагов создания класса и три — создания экземпляра."""
header("1. Порядок вызовов")
class Meta(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs): # noqa: N804
LOG.append("Meta.__prepare__ — выдаёт словарь под тело класса")
return {}
def __new__(mcls, name, bases, namespace, **kwargs): # noqa: N804
LOG.append("Meta.__new__ — создаёт объект класса")
return super().__new__(mcls, name, bases, namespace)
def __init__(cls, name, bases, namespace, **kwargs):
LOG.append("Meta.__init__ — дописывает уже созданный класс")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
LOG.append("Meta.__call__ — перехват СОЗДАНИЯ ЭКЗЕМПЛЯРА")
return super().__call__(*args, **kwargs)
class Field:
def __set_name__(self, owner, name):
LOG.append(f"__set_name__ у дескриптора {name}")
class Base(metaclass=Meta):
def __init_subclass__(cls, **kwargs):
LOG.append("__init_subclass__ у РОДИТЕЛЯ")
super().__init_subclass__(**kwargs)
LOG.clear()
class Child(Base):
LOG.append("тело класса выполняется")
field = Field()
def __new__(cls):
LOG.append("Child.__new__")
return super().__new__(cls)
def __init__(self):
LOG.append("Child.__init__")
show("создание класса")
Child()
show("создание экземпляра")
print("Читать так: __set_name__ и __init_subclass__ вызываются ВНУТРИ")
print("type.__new__, то есть до Meta.__init__. Значит, __init_subclass__")
print("не может увидеть то, что метакласс делает в __init__, а метакласс")
print("в __init__ уже видит результат обоих.")
def observe_type_of_type() -> None:
"""Метакласс — это просто тип класса."""
header("2. Что такое метакласс")
class Meta(type):
pass
class WithMeta(metaclass=Meta):
pass
class Plain:
pass
print(f"type(Plain) = {type(Plain).__name__}")
print(f"type(WithMeta) = {type(WithMeta).__name__}")
print(f"type(type) = {type(type).__name__}")
print(f"Meta — подкласс type: {issubclass(Meta, type)}")
print()
print("Никакой отдельной сущности «метакласс» в языке нет. Есть тип, чьи")
print("экземпляры — классы. Отсюда и весь протокол: __call__ метакласса")
print("вызывается при создании ЭКЗЕМПЛЯРА класса ровно потому, что класс —")
print("экземпляр метакласса.")
def observe_prepare() -> None:
"""То, чего не умеет ничто, кроме метакласса."""
header("3. __prepare__: перехват ТЕЛА класса")
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): # noqa: N804
return NoDuplicates()
def __new__(mcls, name, bases, namespace, **kwargs): # noqa: N804
return super().__new__(mcls, name, bases, dict(namespace))
class Plain:
timeout = 1
timeout = 2 # noqa: F811
print(f"обычный класс: дубль имени проходит молча, timeout = {Plain.timeout}")
try:
class Config(metaclass=StrictMeta):
timeout = 1
timeout = 2 # noqa: F811
except TypeError as exc:
print(f"с __prepare__: {exc}")
print()
print("Это и есть ответ на вопрос «зачем нужен метакласс, если есть")
print("__init_subclass__»: перехватить САМО ВЫПОЛНЕНИЕ тела класса можно")
print("только через __prepare__, а он бывает только у метакласса.")
def observe_init_subclass_limits() -> None:
"""Что умеет и чего не умеет замена без метакласса."""
header("4. Граница __init_subclass__")
class Registry:
registered: list[str] = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Registry.registered.append(cls.__name__)
class First(Registry):
pass
class Second(Registry):
pass
print(f"зарегистрировались: {Registry.registered}")
print("сам Registry в список НЕ попал — хук вызывается у родителя,")
print("когда создаётся ПОТОМОК, и на себя не срабатывает.")
print()
print("Метакласс срабатывает и на том классе, где объявлен.")
def observe_conflict() -> None:
"""Цена метакласса, которая платится не временем."""
header("5. Конфликт метаклассов")
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
try:
class C(A, B):
pass
except TypeError as exc:
print(f"TypeError: {exc}")
print()
print("Ошибка возникает при СОЗДАНИИ класса-наследника, то есть у того,")
print("кто ваш метакласс не выбирал. Это и есть настоящая цена: чужой")
print("код перестаёт наследоваться от вашего вместе с чем угодно ещё,")
print("у чего свой метакласс — например, с ABC или с Enum.")
def observe_decorator() -> None:
"""Третий инструмент — и его две границы."""
header("6. Декоратор класса рядом с остальными")
def decorate(cls):
LOG.append(f"декоратор класса {cls.__name__}")
return cls
class Field:
def __set_name__(self, owner, name):
LOG.append(f"__set_name__ у {name}")
class Base:
def __init_subclass__(cls, **kwargs):
LOG.append(f"__init_subclass__ для {cls.__name__}")
super().__init_subclass__(**kwargs)
LOG.clear()
@decorate
class Decorated(Base):
first = Field()
class Inheritor(Decorated):
second = Field()
show("объявление декорированного класса и наследника")
print("Две границы видны прямо в списке:")
print(" - декоратор вызывается ПОСЛЕДНИМ, когда класс уже полностью готов;")
print(" - на наследника он не распространяется вовсе.")
def observe_namespace() -> None:
"""Что именно лежит в пространстве имён, которое получает метакласс."""
header("7. Пространство имён и базы — они меняются между версиями")
seen: dict = {}
class Watch(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs): # noqa: N804
seen[f"{name}:bases"] = tuple(b.__name__ for b in bases)
return {}
def __new__(mcls, name, bases, namespace, **kwargs): # noqa: N804
seen[f"{name}:namespace"] = sorted(namespace)
return super().__new__(mcls, name, bases, namespace)
class Plain(metaclass=Watch):
value = 1
print(f"обычный класс, базы: {seen['Plain:bases']}")
print(f"обычный класс, пространство: {seen['Plain:namespace']}")
# Обобщённый класс объявляется через exec: на 3.11 синтаксис class C[T]
# не разбирается вовсе, и файл не скомпилировался бы целиком.
if sys.version_info >= (3, 12):
exec(
"class Generic1[T](metaclass=Watch):\n value = 1\n",
{"Watch": Watch},
)
print(f"class C[T], базы: {seen['Generic1:bases']}")
print(f"class C[T], пространство: {seen['Generic1:namespace']}")
else:
print("class C[T]: синтаксиса нет до 3.12")
print()
print("Метакласс, который перебирает пространство имён и падает на")
print("незнакомом ключе, ломается при обновлении. Между 3.12 и 3.13")
print("там появились __firstlineno__ и __static_attributes__, а у")
print("обобщённого класса в базах оказывается typing.Generic, которого")
print("автор класса не писал.")
def main() -> None:
print(f"Python {sys.version.split()[0]}")
observe_order()
observe_type_of_type()
observe_prepare()
observe_init_subclass_limits()
observe_conflict()
observe_decorator()
observe_namespace()
if __name__ == "__main__":
main()