Deep Engineering

MEASUREMENT

bench/context-managers/suppress.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/context-managers
How to run it
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v 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

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

скрипт что показывает
protocol.py два метода на ТИПЕ, что связывает as, почему методы на экземпляре не считаются
suppress.py истина из __exit__ глушит исключение; return True «ради успеха» глотает всё подряд
silent.py ошибка, которая не падает: suppress вокруг цикла даёт 100 вместо 175
generator_cm.py @contextmanager без try/finally, исключение бросается в точку yield, один объект — один вход, LIFO у ExitStack, suppress выходит из блока целиком
traps.py упавший __enter__ не зовёт __exit__; несколько менеджеров — это вложение; сообщение TypeError по версиям
bytecode.py BEFORE_WITH против LOAD_SPECIAL и порядок __exit____enter__ в 3.14
cost.py цена трёх форм: без with, класс, @contextmanager

Запускать на всех версиях, которые есть:

for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v 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, protocol.py, suppress.py, generator_cm.py и traps.py от тулчейна не зависят: они смотрят на имена инструкций, порядок вызовов и текст сообщений. Эти наблюдения честны между любыми версиями.

Разброс между запусками

cost.py — лучшее из семи прогонов по 200 000 итераций. Повторные запуски дают разброс около ±4 % у формы с классом и около ±3 % у @contextmanager. Числа в уроке округлены до целых наносекунд, и выводы построены на кратностях (во сколько раз одна форма дороже другой), а не на последней цифре: кратности разброс переживают, а цифра — нет.

Script

54 lines
import sys
print("PY", sys.version.split()[0])

# 1. Возврат истины из __exit__ ГЛУШИТ исключение. Это единственный способ
#    выйти из with, будто ничего не было.
class Swallow:
    def __exit__(self, exc_type, exc, tb):
        return True
    def __enter__(self): return self

reached = False
with Swallow():
    raise ValueError("это исключение не выйдет наружу")
    reached = True          # недостижимо
print("1) выполнение продолжилось после with:", "да")
print("   строка ПОСЛЕ raise внутри with выполнилась:", reached)

# 2. Самая частая случайная версия: `return` вместо ничего.
#    `return True` в конце __exit__ ради «всё ок» глушит ЛЮБУЮ ошибку.
class LooksHarmless:
    def __enter__(self): return self
    def __exit__(self, *a):
        return True         # хотели «успешно закрылись»

with LooksHarmless():
    1 / 0
print("2) ZeroDivisionError проглочен молча")

# 3. А None (и любая ложь) — пропускает дальше.
class Passthrough:
    def __enter__(self): return self
    def __exit__(self, *a): return None
try:
    with Passthrough():
        raise KeyError("k")
except KeyError:
    print("3) __exit__ вернул None -> исключение прошло наружу")

# 4. __exit__ ВИДИТ исключение, даже если пропускает его.
seen = []
class Watcher:
    def __enter__(self): return self
    def __exit__(self, exc_type, exc, tb):
        seen.append((exc_type.__name__ if exc_type else None, str(exc)))
        return False
try:
    with Watcher():
        raise RuntimeError("вижу тебя")
except RuntimeError:
    pass
with Watcher():
    pass
print("4) что видел __exit__:", seen)