ЗАМЕР
bench/metaclass/decorator_capabilities.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/metaclasses
- Как запустить
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
Запись прогона
Замеры для урока «Метаклассы»
| Скрипт | Что делает |
|---|---|
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. Ошибка при этом появляется
в чужом файле.
Скрипт
169 строк"""Что декоратор класса может и чего не может — по прогону, а не по таблице.
ЗАЧЕМ ЭТОТ ФАЙЛ. В уроке стояла таблица возможностей, где у декоратора класса
было «не может изменить имя, базы или пространство имён». Аудит возразил:
декоратор получает ГОТОВЫЙ объект класса, а с готовым классом можно многое —
и переименовать, и подменить целиком. Прав он или нет, решает не спор, а
запуск: у каждой строки таблицы здесь есть свой блок, и печатается то, что
получилось на самом деле, включая тип ошибки, если её не миновать.
ГРАНИЦА, КОТОРУЮ ЭТОТ ФАЙЛ ПРОВОДИТ. Она проходит не по «может/не может», а по
ВРЕМЕНИ: до создания класса (пространство имён, базы, аргументы type.__new__)
или после (готовый объект). Декоратор всегда после; метакласс — и до, и во
время, и после. Поэтому таблица должна сравнивать не силу, а момент.
ЗАПУСК: python3.13 bench/metaclass/decorator_capabilities.py
Вывод по версиям: runs/decorator_capabilities-3.11.txt и соседние.
"""
import sys
EVENTS: list[str] = []
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def result(label: str, value: object) -> None:
print(f" {label:<52} {value}")
def attempt(label: str, fn) -> None:
"""Печатает результат или ТИП ошибки.
Текст сообщений между версиями меняется, тип — нет; в таблице урока речь о
возможности, а не о формулировке.
"""
try:
result(label, fn())
except Exception as exc: # noqa: BLE001 — тип и есть ответ
result(label, f"{type(exc).__name__}")
# ------------------------------------------------------------------ 1
def block1() -> None:
show("1. Декоратор получает класс уже созданным")
class Watcher:
def __set_name__(self, owner, name):
EVENTS.append(f"__set_name__ увидел класс с именем {owner.__name__!r}")
def decorator(cls):
EVENTS.append(f"декоратор получил класс с именем {cls.__name__!r}")
return cls
EVENTS.clear()
@decorator
class Sample:
field = Watcher()
for line in EVENTS:
result("порядок:", line)
result("то есть декоратор работает:", "после __set_name__ и после type.__new__")
# ------------------------------------------------------------------ 2
def block2() -> None:
show("2. Что декоратор с готовым классом всё-таки может")
def rename(cls):
cls.__name__ = "Переименован"
cls.added = "добавлено декоратором"
return cls
@rename
class Sample:
original = "было в теле"
result("__name__ после декоратора", Sample.__name__)
result("новый атрибут виден", Sample.added)
result("старый атрибут на месте", Sample.original)
def replace(cls):
return type("Подменённый", (), {"kind": "совсем другой объект"})
@replace
class Other:
kind = "из тела класса"
result("декоратор вернул другой класс: имя", Other.__name__)
result("и его содержимое", Other.kind)
# ------------------------------------------------------------------ 3
def block3() -> None:
show("3. Базы: присваивание __bases__ разрешено не всегда")
class Base:
pass
class Another:
pass
class OnObject:
pass
class OnBase(Base):
pass
attempt(
"класс на object: __bases__ = (Base,)",
lambda: (setattr(OnObject, "__bases__", (Base,)), OnObject.__mro__[1].__name__)[1],
)
attempt(
"класс на Base: __bases__ = (Another,)",
lambda: (setattr(OnBase, "__bases__", (Another,)), OnBase.__mro__[1].__name__)[1],
)
result("вывод", "«не может менять базы» — не про декоратор, а про правила смены __bases__")
# ------------------------------------------------------------------ 4
def block4() -> None:
show("4. Пространство имён: единственное, чего декоратор не застаёт")
class OrderedNamespace(dict):
def __setitem__(self, key, value):
EVENTS.append(f"в пространство имён кладут {key!r}")
super().__setitem__(key, value)
class Meta(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
EVENTS.append("__prepare__ дал своё отображение")
return OrderedNamespace()
def __new__(mcls, name, bases, ns, **kwargs):
EVENTS.append(f"Meta.__new__ видит имена: {[k for k in ns if not k.startswith('__')]}")
return super().__new__(mcls, name, bases, dict(ns), **kwargs)
EVENTS.clear()
def decorator(cls):
EVENTS.append("декоратор: тело уже выполнено, класс собран")
return cls
@decorator
class Sample(metaclass=Meta):
a = 1
b = 2
for line in EVENTS:
result("порядок:", line)
result("вывод", "тело выполняется в отображении из __prepare__ — до декоратора")
def main() -> None:
print(f"Python {sys.version.split()[0]} ({sys.implementation.name})")
block1()
block2()
block3()
block4()
if __name__ == "__main__":
main()