MEASUREMENT
bench/decorators/cost.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/decorators
- How to run it
python3.13 bench/decorators/cost.py python3.14 bench/decorators/cost.py python3.13 bench/decorators/wraps_signature.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
Замеры для урока «Декораторы»
| Скрипт | Что меряет |
|---|---|
cost.py |
цену слоёв обёртки: 0, 1, 2 и 3 слоя *args, **kwargs с functools.wraps над одной и той же функцией |
wraps_signature.py |
что на самом деле показывает inspect.signature у обёртки с wraps и что меняет follow_wrapped=False |
python3.13 bench/decorators/cost.py
python3.14 bench/decorators/cost.py
python3.13 bench/decorators/wraps_signature.py
Каталог runs/ — записи прогонов
В runs/ лежит дословный вывод скриптов, без правок и без пересказа. Это не
украшение: на этих файлах стоят практические задачи урока. Ответ задачи —
строка из записи прогона, и сборка сайта сверяет одно с другим
(scripts/validate-practice.mjs): задача, чей ответ разошёлся с прогоном или
чей скрипт исчез, не собирается.
Прогоны сняты 30.08.2026 на CPython 3.13.7 (Clang 20.1.4) и 3.14.7
(Clang 22.1.3), в песочнице разработки. Числа урока взяты из ЭТИХ же прогонов
— таблица, заблуждение, визуализация WrapperCost и практическая задача
показывают одну четвёрку, а не три разных, как было до 30.08.
Абсолютные наносекунды всё равно принадлежат машине: на другой они будут другими. Переносится кратность, и практическая задача стоит именно на ней.
Перезаписывать запись прогона имеет смысл только вместе с проверкой задач: если после перезапуска ответ изменился, изменить нужно и задачу, а не файл.
Что здесь важно прочитать правильно
Меряется не «дорог ли декоратор», а во что обходится каждый следующий слой. Наносекунды на слой у каждой машины свои — в записи прогона это около 45 нс. Устойчиво здесь другое: рост линейный, и три слоя дают примерно семикратную цену вызова, то есть около семи восьмых времени уходит на обёртки, а не на работу. Именно эта кратность и переносится на другую машину.
Кратность устойчива только потому, что формы меряются вперемежку. Пока они мерились подряд, отношение трёх слоёв к голому вызову выходило 5,9 / 7,1 / 7,5 / 9,2 на четырёх запусках — просадка машины в окне одной формы целиком доставалась ей. После перехода на чередующиеся круги тот же замер даёт 7,1–7,7. Это правило теперь общее для всех скриптов с временем.
Общее правило замеров: время между версиями не сравнивается вообще. Сравнивается только измеренное внутри одного запуска одного интерпретатора. Числа 3.13.7 и 3.14.7 приводятся рядом как два независимых результата, а не как сравнение — цена слоя внутри каждого прогона своя, и именно она переносится на другую машину.
Чего этот замер не показывает
Времени применения декоратора. Оно тратится один раз при импорте и в горячем пути не участвует — а именно горячий путь и есть предмет урока. Порядок применения декораторов и связывание имени разобраны в уроке через дизассемблер, а не через время: там доказательство не наносекунды, а последовательность инструкций.
Script
86 lines"""Сколько стоит каждый слой декоратора.
ПОЧЕМУ ЭТОТ ФАЙЛ ПОЯВИЛСЯ ПОЗЖЕ УРОКА. Числа обёрток были в уроке и в
визуализации `WrapperCost`, а скрипта, которым они получены, в репозитории не
было — то есть проверить их читатель не мог, хотя весь сайт обещает обратное.
Файл закрывает эту дыру: он и есть источник четырёх чисел.
ЧТО МЕРЯЕТСЯ. Одна и та же функция под нулём, одним, двумя и тремя слоями
обычной обёртки `*args, **kwargs` с `functools.wraps`. Каждый слой добавляет
вызов Python-функции и упаковку аргументов, поэтому рост должен быть линейным —
и в этом всё содержание замера.
ОБЩЕЕ ПРАВИЛО ЗАМЕРОВ: время между версиями не сравнивается вообще. Все
четыре формы измеряются одним процессом, и сравнивать их между собой можно
всегда.
ПОЧЕМУ ЗАМЕРЫ ЧЕРЕДУЮТСЯ, А НЕ ИДУТ ПОДРЯД. Первая версия мерила формы одну
за другой: сначала семь повторов голого вызова, потом семь повторов одного
слоя и так далее. На машине с переменной нагрузкой это даёт не шум, а СДВИГ:
просадка, случившаяся в окне одной формы, целиком достаётся ей, и кратность
между формами меняется от запуска к запуску. Измерено на четырёх запусках
подряд: отношение трёх слоёв к голому вызову вышло 5,9 / 7,1 / 7,5 / 9,2 —
полуторакратный разброс там, где меряется одно и то же.
Теперь формы чередуются: в каждом круге меряются все четыре, и минимум для
каждой берётся по кругам. Просадка круга бьёт по всем четырём сразу, поэтому
кратность её переживает, а минимум по кругам остаётся оценкой наименее
потревоженного выполнения.
"""
import functools
import sys
import timeit
N = 20_000
REPEAT = 100
def target(x):
return x + 1
def wrap(fn):
@functools.wraps(fn)
def inner(*args, **kwargs):
return fn(*args, **kwargs)
return inner
one = wrap(target)
two = wrap(wrap(target))
three = wrap(wrap(wrap(target)))
ROWS = [
("без декораторов", "target(1)"),
("один слой", "one(1)"),
("два слоя", "two(1)"),
("три слоя", "three(1)"),
]
def measure(stmts):
"""Минимум по кругам для каждой формы; в каждом круге меряются все формы."""
setup = "from __main__ import target, one, two, three"
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
print("PY", sys.version.split()[0], f"| лучшее из {REPEAT} чередующихся кругов по {N:,} вызовов".replace(",", " "))
values = [t * 1e9 for t in measure([stmt for _, stmt in ROWS])]
for (label, _), ns in zip(ROWS, values):
print(f" {label:<20} {ns:7.1f} нс")
base = values[0]
print(" прибавка на слой:")
for i in range(1, len(values)):
print(f" слой {i}: +{values[i] - values[i - 1]:6.1f} нс")
print(f" доля работы самой функции при трёх слоях: {base / values[-1] * 100:.0f} %")
# Кратность печатается отдельной строкой, потому что именно она переносится на
# другую машину, а наносекунды — нет. На ней же стоит практическая задача урока
# («во сколько раз»), и сборочная проверка ищет это число здесь.
print(f" три слоя дороже голого вызова в {values[-1] / base:.1f} раза")