Урок по треку Python 7 мин

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

try/except/else/finally, иерархия исключений, свои ошибки, ExceptionGroup, with и contextlib.

Хорошая обработка ошибок отличает 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*.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.