Deep Engineering

ЗАМЕР

bench/mro/practice.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/python/mro
Как запустить
python3.11 bench/mro/order.py
python3.12 bench/mro/order.py
python3.13 bench/mro/order.py
python3.14 bench/mro/order.py

Запись прогона

Замеры для урока «MRO и множественное наследование»

Скрипт Что делает
order.py наблюдения без времени: слияние C3 по шагам и сверка с __mro__ на семи иерархиях, отказ C3 дословным текстом, куда ведёт super() у экземпляров разных классов, оборванная цепочка, кооперативный __init__, ячейка __class__
practice.py источник ответов двух практических задач урока

Время поиска по длинному MRO здесь не меряется заново: оно снято в bench/attr-lookup/mro_cache.py (с кешем атрибутов типа и без), а влияние переопределённого mro() — в bench/attr-lookup/mro_override.py. Урок ссылается на те записи прогонов.

python3.11 bench/mro/order.py
python3.12 bench/mro/order.py
python3.13 bench/mro/order.py
python3.14 bench/mro/order.py

Записи в runs/: order.txt — 3.13.7, остальные версии — с суффиксом.

Что получилось

Ручной C3 совпал с интерпретатором на всех семи иерархиях — ромб и шесть иерархий с несколькими базами, одна из них по мотивам примера из Doc/howto/mro.rst. Реализация в скрипте и есть то правило, которое урок цитирует: голова первого списка, если её нет ни в одном хвосте.

Вывод одинаков на 3.11–3.14, кроме одного места. На 3.11.15 и 3.12.3 текст TypeError об отказе C3 содержит перевод строки: «Cannot create a consistent method resolution» и «order (MRO) for bases X, Y» печатаются двумя строками. На 3.13.7 и 3.14.7 — одной. Это видно простым diff записей.

Ячейка __class__ есть у функции, объявленной в теле класса (co_freevars == ('__class__',)), и отсутствует у функции, приписанной классу снаружи: super() без аргументов в ней падает с RuntimeError: super(): __class__ cell not found на всех четырёх версиях.

Практика урока

practice.py — источник ответов двух задач, runs/practice.txt — дословная запись его прогона на 3.13.7; сборка сверяет с ней ответы (scripts/validate-practice.mjs).

Вторая задача — отношение времени o.m при длине MRO 51 и 2. Это частное двух величин около 30 нс, и оно шумит: на четырёх версиях вышло 1,0–1,1 (runs/practice-3.11.txt и соседние). Поэтому у задачи широкий допуск, а утверждение урока строится не на нём, а на наклоне из mro_cache.py: +0,02 нс на уровень с кешем против +3,31 без.

Скрипт

88 строк
"""Практика урока «MRO и множественное наследование»: источник ответов двух задач.

Ответ задачи не сочиняется редакцией — он берётся из записи прогона этого
скрипта (`runs/practice.txt`), а сборка сверяет одно с другим
(`scripts/validate-practice.mjs`).

ЧТО ПРОВЕРЯЮТ ЗАДАЧИ. Первая — куда ведёт `super()`: к следующему классу в
MRO экземпляра, а не к родителю того класса, где он написан. В ромбе поэтому
выполняется и соседняя ветка, и общий предок — ровно один раз.

Вторая — цена длинного MRO. Интуиция говорит «поиск идёт по списку, значит,
длиннее список — дороже вызов». Кеш атрибутов типа отвечает на повторное
обращение, не проходя список вовсе, и разница между MRO из двух классов и из
пятидесяти одного почти пропадает. Без кеша она есть, и большая, — это снято
отдельно в bench/attr-lookup/mro_cache.py.

ПОЧЕМУ ЗАМЕРЫ ЧЕРЕДУЮТСЯ. Просадка машины в окне одной формы иначе целиком
достаётся ей, и кратность гуляет от запуска к запуску. Здесь в каждом круге
меряются обе формы, минимум для каждой берётся по кругам.

Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import sys
import timeit

N = 200_000
REPEAT = 9


class Base:
    def hello(self):
        print("Base")


class A(Base):
    def hello(self):
        print("A")
        super().hello()


class B(Base):
    def hello(self):
        print("B")
        super().hello()


class C(A, B):
    pass


print("PY", sys.version.split()[0])
print()
print("вывод задачи:")
C().hello()
print()


def chain(depth):
    """Цепочка из depth классов над object; метод объявлен у самого верхнего,
    так что поиск без кеша прошёл бы весь MRO."""
    top = type("L0", (), {"m": lambda self: None})
    cls = top
    for i in range(1, depth - 1):
        cls = type(f"L{i}", (cls,), {})
    return cls


Short = chain(2)
Long = chain(51)
short, long_ = Short(), Long()
assert len(Short.__mro__) == 2 and len(Long.__mro__) == 51

setup = "from __main__ import short, long_"


def measure(*stmts):
    best = [float("inf")] * len(stmts)
    for _ in range(REPEAT):
        for i, stmt in enumerate(stmts):
            best[i] = min(best[i], timeit.timeit(stmt, setup=setup, number=N) / N)
    return best


s, l = [t * 1e9 for t in measure("short.m", "long_.m")]
print(f"o.m, длина __mro__ 2:    {s:5.1f} нс")
print(f"o.m, длина __mro__ 51:   {l:5.1f} нс")
print(f"  MRO из 51 класса дороже MRO из 2 в {l / s:.1f} раза")