Хорошая обработка ошибок отличает production-код от скрипта. На собесе проверяют порядок выполнения try/except/else/finally, иерархию исключений и умение написать свой контекстный менеджер.
Полная форма try
try:
data = load()
except FileNotFoundError:
data = {}
except (ValueError, KeyError) as e:
log.warning('битые данные: %s', e)
raise
else:
print('загружено без ошибок') # только если в try не было исключения
finally:
close_all() # всегда, даже при return и raise
exceptпроверяются сверху вниз, срабатывает первый подходящий. Поэтому конкретные исключения пишут выше общих.elseвыполняется, еслиtryпрошёл без исключения. Туда кладут код, ошибки которого не надо ловить этим жеexcept.finallyвыполняется всегда. Если вfinallyестьreturn, он перекроет и возвращаемое значение, и летящее исключение (поэтому так не пишут; с 3.14 на это есть предупреждение).
Иерархия
BaseException
├── SystemExit, KeyboardInterrupt, GeneratorExit
└── Exception
├── ValueError, TypeError, KeyError, IndexError, ...
├── OSError (FileNotFoundError, TimeoutError, ConnectionError...)
└── ваши исключения
except Exception не ловит KeyboardInterrupt и SystemExit, и это правильно. Голый except: ловит всё, включая Ctrl+C, это почти всегда ошибка.
Свои исключения и цепочки
class PaymentError(Exception):
"""Базовое исключение модуля оплаты."""
class CardDeclined(PaymentError):
pass
try:
gateway.charge()
except httpx.TimeoutException as e:
raise PaymentError('шлюз не ответил') from e
raise ... from e сохраняет причину в __cause__, и traceback покажет обе ошибки. Внутри except без from причина всё равно попадёт в __context__ («During handling of the above exception...»). from None скрывает контекст.
Голый raise внутри except пробрасывает текущее исключение с исходным traceback. С 3.11 к исключению можно добавить пояснение: e.add_note('order_id=42').
ExceptionGroup и except*
С Python 3.11 несколько независимых ошибок можно бросить разом: ExceptionGroup('msg', [e1, e2]). Так делает asyncio.TaskGroup, когда падают несколько задач. Ловят их через except*: каждая ветка обрабатывает свою часть группы.
try:
async with asyncio.TaskGroup() as tg: ...
except* TimeoutError as eg:
...
except* ValueError as eg:
...
EAFP против LBYL
Python-стиль: «проще попросить прощения, чем разрешения» (EAFP). Вместо if key in d: x = d[key] пишут try: x = d[key] except KeyError: .... Проверка заранее (LBYL) в многопоточном коде ещё и создаёт гонку: файл может исчезнуть между проверкой и открытием.
Контекстные менеджеры
with гарантирует освобождение ресурса. Протокол: __enter__ (его результат попадает в as) и __exit__(exc_type, exc, tb). Если __exit__ вернул истину, исключение подавляется.
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self.start
return False # исключения не глотаем
Короче через генератор:
from contextlib import contextmanager
@contextmanager
def transaction(conn):
conn.begin()
try:
yield conn
except Exception:
conn.rollback()
raise
else:
conn.commit()
Полезное из contextlib: suppress(FileNotFoundError), closing, ExitStack (динамическое число ресурсов), nullcontext, асинхронные asynccontextmanager и async with.
Частые ошибки
except Exception: pass: ошибка проглочена, отладить невозможно.raise eвместоraise: traceback начнётся с этой строки (и лишний кадр).returnвfinally.- Широкий
tryна весь метод вместо одной опасной строки. @contextmanagerбезtry/finallyвокругyield: при исключении очистка не выполнится.
Что спросят на собесе
- Порядок выполнения
try/except/else/finally, что вернёт функция сreturnвtryи вfinally. - Чем
except Exceptionотличается отexcept:иexcept BaseException. - Как сделать своё исключение, зачем
raise from. - Как работает
with, как написать контекстный менеджер двумя способами. - Что такое
ExceptionGroupиexcept*.