MEASUREMENT
bench/exceptions/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/exceptions-and-finally
- How to run it
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v finally_traps.py; done
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
Замеры для урока «Исключения и finally»
| скрипт | что показывает |
|---|---|
finally_traps.py |
return/break в finally проглатывают исключение и перекрывают возврат; SyntaxWarning в 3.14 (PEP 765) |
silent.py |
ошибка, которая не падает: 100 вместо 175 — те же строки, что в замере про контекстные менеджеры |
chaining.py |
__context__, __cause__, raise ... from None, исчезновение имени после except ... as e |
scope.py |
голый except: ловит BaseException; что делает else и почему его тело не защищено |
groups.py |
ExceptionGroup и except*: один raise — несколько срабатываний |
bytecode.py |
почему try бесплатен, пока не сработал: нет SETUP_FINALLY, есть ExceptionTable |
cost.py |
цена трёх форм: без try, try не сработал, try сработал |
Запускать на всех версиях, которые есть:
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v finally_traps.py; done
Практика урока
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). После перехода на чередование расхождение между прогонами не выходит за несколько процентов. Перезаписывать запись прогона имеет смысл только вместе с проверкой задачи: если после перезапуска ответ изменился, менять нужно задачу, а не файл.
Что здесь сравнивать можно, а что нельзя
Общее правило замеров: время между версиями не сравнивается вообще.
Сравнивается только то, что измерено внутри одного запуска одного
интерпретатора. Дело не только в компиляторе: 3.11 и 3.12 собраны GCC 13.3.0,
3.13.7 и 3.14.7 — Clang 20.1.4, но и эти две сборки различаются между собой,
причём ровно тем флагом (--with-tail-call-interp), которому «Что нового в
3.14» приписывает «a geometric mean of 3-5% faster».
Поэтому cost.py сравнивает три формы внутри одного запуска, а числа
3.13.7 и 3.14.7 стоят рядом как два независимых результата. Сравнения по
времени с 3.11 и 3.12 в уроке нет.
bytecode.py, finally_traps.py, chaining.py, scope.py и groups.py от
тулчейна не зависят: они смотрят на имена инструкций, наличие таблицы
исключений, порядок вызовов и текст сообщений. Эти наблюдения честны между
любыми версиями — и именно они объясняют числа из cost.py.
Разброс между запусками
cost.py — лучшее из семи прогонов по 200 000 итераций. Повторные запуски
дают разброс около ±5 %. Числа в уроке округлены до целых наносекунд, а
главный вывод — что «не сработавший try» и «без try» неразличимы, —
разброс переживает: разница между ними меньше разброса каждого из них, и
это и есть утверждение.
Откуда ±5 %, и почему раньше стояло ±3 %. 21 августа 2026 сделано по пять
свежих прогонов cost.py на каждой версии. Строка «без try» дала
25,7–26,9 нс на 3.13.7 и 26,6–28,1 нс на 3.14.7 — размах 1,2 и 1,5 нс.
Прежняя оценка ±3 % (то есть ±0,8 нс) была меньше разрыва между первыми
двумя строками на 3.14 (1,2 нс): подпись к ExceptionCost утверждала «разницы
нет» вопреки собственной арифметике. С ±5 % арифметика сходится на обеих
версиях.
Сильнее любого процента здесь другое наблюдение: в одном из пяти прогонов на
3.13.7 строки поменялись местами — 26,9 нс без try против 26,7 нс с ним.
Порядок, который меняется от запуска к запуску, и есть определение
«неразличимо».
Script
78 lines"""Сколько стоит try/except, когда исключения нет — и когда есть.
ПРАВИЛО (bench/README.md): время между версиями не сравнивается вообще.
Сравнивается только измеренное внутри одного запуска одного интерпретатора —
все сравнения ниже такие. Числа 3.13.7 и 3.14.0rc2 приводятся рядом как два
независимых результата, а не как сравнение.
"""
import sys, timeit
N = 200_000
REPEAT = 7
def plain(x):
return x + 1
def guarded(x):
try:
return x + 1
except ValueError:
return 0
def raising(x):
try:
raise ValueError(x)
except ValueError:
return 0
def best(stmt, setup):
return min(timeit.repeat(stmt, setup=setup, number=N, repeat=REPEAT)) / N * 1e9
setup = "from __main__ import plain, guarded, raising"
ROWS = [
("без try", "plain(1)"),
("try/except, не сработал", "guarded(1)"),
("try/except, сработал", "raising(1)"),
]
# СКОЛЬКО РАЗ ПОВТОРЯЕТСЯ ВЕСЬ ЗАМЕР, И ЗАЧЕМ.
#
# Урок утверждает не только числа, но и то, КАК их читать: разница между
# первыми двумя строками сопоставима с разбросом прибора или нет. Долгое время
# это утверждение держалось на разборе руками — пять запусков, размахи 3,6 и
# 4,9 нс, — которого в репозитории не было. Утверждение о методе обязано
# держаться на прогоне так же, как число: теперь весь замер повторяется здесь,
# и прогон сам печатает размах каждой строки и вердикт.
OUTER = 5
print("PY", sys.version.split()[0], f"| лучшее из {REPEAT} прогонов по {N:,} итераций")
series = {name: [] for name, _ in ROWS}
for _ in range(OUTER):
for name, stmt in ROWS:
series[name].append(best(stmt, setup))
for name, _ in ROWS:
values = series[name]
print(f" {name:<26} {min(values):8.1f} нс")
print()
print(f" РАЗМАХ ПО {OUTER} ПОВТОРАМ ВСЕГО ЗАМЕРА")
for name, _ in ROWS:
values = series[name]
print(f" {name:<26} {min(values):8.1f} .. {max(values):6.1f} нс размах {max(values) - min(values):5.1f}")
gap = min(series["try/except, не сработал"]) - min(series["без try"])
spread = max(
max(series["без try"]) - min(series["без try"]),
max(series["try/except, не сработал"]) - min(series["try/except, не сработал"]),
)
print()
print(f" разница между первыми двумя строками {gap:5.1f} нс")
print(f" наибольший размах этих же двух строк {spread:5.1f} нс")
print(f" различима ли разница прибором {'да' if gap > spread else 'нет'}")
print()
print(" Читать надо эти три строки, а не первые две. Пока разница между")
print(" строками меньше размаха самой строки, говорить о ней нельзя вовсе;")
print(" когда больше — можно, и тогда вопрос уже не «есть ли разница», а")
print(" «стоит ли она того», а это вопрос к её величине, а не к её знаку.")