ЗАМЕР
bench/context-managers/silent.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.
Числа в уроке округлены до целых наносекунд, и выводы построены на
кратностях (во сколько раз одна форма дороже другой), а не на последней
цифре: кратности разброс переживают, а цифра — нет.
Скрипт
57 строк"""Ошибка, которая не падает.
Как и в уроке про итераторы: интересна не та ошибка, что роняет программу,
а та, что молча возвращает неверный ответ.
"""
import sys
from contextlib import suppress
print("PY", sys.version.split()[0])
ROWS = [
{"id": 1, "amount": "100"},
{"id": 2, "amount": "нет данных"}, # битая строка
{"id": 3, "amount": "50"},
{"id": 4, "amount": "25"},
]
# ВЕРНО: suppress вокруг КАЖДОЙ строки — пропускаем битую, считаем остальные.
def per_row(rows):
total = 0
skipped = 0
for row in rows:
with suppress(ValueError):
total += int(row["amount"])
continue
skipped += 1
return total, skipped
# НЕВЕРНО, и выглядит почти так же: suppress вокруг ЦИКЛА. Первая же битая
# строка выходит из блока целиком — остальные строки не считаются вовсе.
def whole_loop(rows):
total = 0
with suppress(ValueError):
for row in rows:
total += int(row["amount"])
return total
print("1) правильно (suppress на строку):", per_row(ROWS))
print(" неправильно (suppress на цикл):", whole_loop(ROWS))
print(" верная сумма:", 100 + 50 + 25)
print(" разница: потеряно", (100 + 50 + 25) - whole_loop(ROWS), "из", 100 + 50 + 25)
print(" исключений при этом не было ни одного")
# То же самое, но через собственный __exit__, возвращающий True «на всякий
# случай». Здесь ошибка ещё незаметнее: в коде вызова нет ни слова suppress.
class Quiet:
def __enter__(self): return self
def __exit__(self, *a): return True # «чтобы не падало»
def with_quiet(rows):
total = 0
with Quiet():
for row in rows:
total += int(row["amount"])
return total
print("2) собственный __exit__ -> True:", with_quiet(ROWS))