Какие два пути есть у контекстного менеджера при возникновении исключения в Python
Что проверяют
Понимаете ли вы протокол with на уровне __exit__, а не только «файл закроется сам». Главное, что хотят услышать: исключение либо подавляется, либо летит дальше, и решает это возвращаемое значение. Следом часто просят показать то же самое через @contextmanager.
Как отвечать
Если в блоке with возникло исключение, Python вызывает __exit__(exc_type, exc_value, traceback) с данными этого исключения. Без исключения все три аргумента равны None. Дальше два пути:
- подавить:
__exit__возвращает истинное значение. Исключение считается обработанным, выполнение продолжается со строки после блокаwith; - пропустить дальше:
__exit__возвращает ложное значение, чаще всего простоNone. После выхода из метода исключение продолжает подниматься по стеку, и его ловит внешнийtry/except.
Детали, которые показывают глубину:
- перебрасывать полученное исключение внутри
__exit__не нужно, достаточно вернутьFalse. Новое исключение, брошенное в__exit__, заменит исходное, а старое окажется в__context__; - если исключение возникло в
__enter__, то__exit__не вызывается вообще, ресурс считается не захваченным; - в генераторе с
@contextmanagerисключение прилетает в точкуyield. Код очистки должен стоять вfinally, а если перехватить исключение и не пробросить, оно будет подавлено; - готовое решение для подавления:
contextlib.suppress(FileNotFoundError). В асинхронном коде ту же роль играют__aenter__и__aexit__.
Хорошая практика: подавлять только ожидаемые типы, проверяя exc_type через issubclass, а всё остальное пропускать.
Где спотыкаются
Возвращают True без проверки типа. Такой менеджер молча глотает любые ошибки, включая KeyboardInterrupt, и баги пропадают без следа.
Пишут @contextmanager без try/finally. При исключении код после yield не выполнится, и соединение или блокировка останутся открытыми.
Называют второй путь «обработкой исключения». Менеджер его не обрабатывает, а только выполняет очистку и отпускает исключение наверх.