Deep Engineering

MEASUREMENT

bench/metaclass/decorator_capabilities.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:

Создание класса — шесть шагов:

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

Создание экземпляра — три:

  1. Meta.__call__
  2. Class.__new__
  3. 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

169 lines
"""Что декоратор класса может и чего не может — по прогону, а не по таблице.

ЗАЧЕМ ЭТОТ ФАЙЛ. В уроке стояла таблица возможностей, где у декоратора класса
было «не может изменить имя, базы или пространство имён». Аудит возразил:
декоратор получает ГОТОВЫЙ объект класса, а с готовым классом можно многое —
и переименовать, и подменить целиком. Прав он или нет, решает не спор, а
запуск: у каждой строки таблицы здесь есть свой блок, и печатается то, что
получилось на самом деле, включая тип ошибки, если её не миновать.

ГРАНИЦА, КОТОРУЮ ЭТОТ ФАЙЛ ПРОВОДИТ. Она проходит не по «может/не может», а по
ВРЕМЕНИ: до создания класса (пространство имён, базы, аргументы 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()