MEASUREMENT
bench/mro/practice.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/mro
- How to run it
python3.11 bench/mro/order.py python3.12 bench/mro/order.py python3.13 bench/mro/order.py python3.14 bench/mro/order.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
Замеры для урока «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 без.
Script
88 lines"""Практика урока «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} раза")