Урок 03 из 12 · 2 минуты чтения

Исключения и контекстные менеджеры

try, except, else и finally, свои исключения и цепочки raise from, with и контекстные менеджеры, ExceptionGroup.

Четыре блока

try:
    data = load(path)          # опасный код — как можно короче
except FileNotFoundError:
    data = default()           # обработка конкретной ошибки
else:
    validate(data)             # только если ошибки не было
finally:
    log.info("done")           # всегда

else полезен тем, что исключения из него не попадают в except выше: так обработка не маскирует ошибки в коде, который к ней не относится.

Ловить конкретное

except: без типа перехватит даже KeyboardInterrupt и SystemExit. except Exception — тоже слишком широко, если вы не логируете и не пробрасываете ошибку дальше. Ловят то, что умеют обработать, остальное пусть летит выше.

Свои исключения

У проекта или библиотеки — один базовый класс и иерархия под ним:

class PaymentError(Exception): ...
class CardDeclined(PaymentError): ...
class ProviderUnavailable(PaymentError): ...

Вызывающему удобно: можно поймать всё семейство except PaymentError или конкретный случай.

Цепочки исключений

Когда низкоуровневую ошибку превращают в понятную, причину сохраняют:

try:
    resp = client.post(url, json=payload, timeout=5)
except httpx.TimeoutException as err:
    raise ProviderUnavailable("платёжный шлюз не ответил") from err

from err кладёт исходную ошибку в __cause__, и в трассировке видно обе. Простой raise внутри except тоже связывает их через __context__, но с пометкой «во время обработки возникла другая ошибка». from None скрывает исходную — уместно редко. С Python 3.11 к исключению можно добавить контекст без обёртки: err.add_note("заказ 42").

Контекстные менеджеры

with гарантирует подготовку и очистку вокруг блока, даже при исключении:

with open("data.csv") as f:
    rows = f.readlines()
# файл закрыт, что бы ни случилось

Протокол — __enter__ при входе и __exit__ при выходе. Проще написать менеджер генератором:

from contextlib import contextmanager

@contextmanager
def timer(name):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(name, time.perf_counter() - start)

Если __exit__ вернёт True, исключение будет подавлено — так делают редко и осознанно (contextlib.suppress). Ресурсы — файлы, соединения, блокировки, транзакции — закрывают через with, а не через __del__.

Несколько ошибок сразу

В конкурентном коде могут упасть несколько задач. asyncio.TaskGroup (3.11) отменяет остальные задачи при первой ошибке и собирает ошибки в ExceptionGroup, а except* разбирает группу по типам:

try:
    async with asyncio.TaskGroup() as tg:
        tg.create_task(fetch_a())
        tg.create_task(fetch_b())
except* httpx.TimeoutException as group:
    log.warning("таймаутов: %d", len(group.exceptions))

asyncio.gather ведёт себя иначе: пробрасывает первую ошибку, а остальные задачи продолжают работать брошенными.

Что запомнить

  • try — короткий, except — конкретный, else — код после успеха.
  • Свои исключения — иерархия от одного базового класса.
  • Перевод ошибки — raise ... from err.
  • Ресурсы — через with.
  • TaskGroup отменяет соседей и возвращает ExceptionGroup.

Проверка урока

Войдите, чтобы проверить себя: сданные проверки всех уроков курса отмечают его темы пройденными на карте и в ИПР.

Вопросы по теме

Читайте также

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования