Deep Engineering

ЗАМЕР

bench/context-managers/suppress.py

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

Цитируется в статье
/ru/interview/python/context-managers
Как запустить
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v traps.py; done

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

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

скрипт что показывает
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. Числа в уроке округлены до целых наносекунд, и выводы построены на кратностях (во сколько раз одна форма дороже другой), а не на последней цифре: кратности разброс переживают, а цифра — нет.

Скрипт

54 строк
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)