PythonMiddleTechnical

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

Блок with гарантирует вызов __exit__ даже при исключении через механизм try/finally. __exit__ получает тип, значение и трейсбек исключения и может его подавить, вернув True.

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

Оператор with реализован через скрытый блок try/finally. Метод __exit__ контекстного менеджера вызывается всегда — независимо от того, завершился ли блок нормально или выбросил исключение.

Внутренняя механика

# Компилятор разворачивает:
# with open("file.txt") as f:
#     data = f.read()

# В примерно такой код:
_mgr = open("file.txt")
f = _mgr.__enter__()
_exc = None
try:
    data = f.read()      # тело блока
except BaseException as e:
    _exc = e
    # __exit__ получает сведения об исключении
    if _mgr.__exit__(type(e), e, e.__traceback__):
        pass  # исключение подавлено (возврат True)
    else:
        raise  # исключение перебрасывается
else:
    _mgr.__exit__(None, None, None)  # нормальный выход

Пример: менеджер с логированием исключений

import logging
from types import TracebackType
from typing import Type


class ManagedResource:
    def __init__(self, name: str) -> None:
        self.name = name
        self._handle: object = None

    def __enter__(self) -> "ManagedResource":
        logging.info("Opening %s", self.name)
        self._handle = object()  # имитация открытия ресурса
        return self

    def __exit__(
        self,
        exc_type: Type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> bool:
        logging.info("Closing %s", self.name)
        self._handle = None  # всегда освобождаем ресурс

        if exc_type is ValueError:
            logging.warning("Suppressing ValueError: %s", exc_val)
            return True  # подавляем ValueError
        return False  # все прочие исключения пробрасываем дальше


with ManagedResource("db-connection") as res:
    raise ValueError("bad input")  # будет подавлено
print("Продолжаем выполнение")    # выведется

try:
    with ManagedResource("db-connection") as res:
        raise RuntimeError("fatal")  # НЕ подавляется
except RuntimeError:
    print("RuntimeError не подавлен, но ресурс закрыт")

contextmanager и обработка исключений

from contextlib import contextmanager
import psycopg2


@contextmanager
def transaction(conn: psycopg2.extensions.connection):
    try:
        yield conn
        conn.commit()              # только при успехе
    except Exception:
        conn.rollback()            # откатываем при любой ошибке
        raise                      # пробрасываем исключение дальше
    finally:
        # finally выполняется всегда — идеально для закрытия
        pass  # conn.close() если нужно

ExitStack для динамического числа ресурсов

from contextlib import ExitStack

files = ["a.txt", "b.txt", "c.txt"]

with ExitStack() as stack:
    handles = [stack.enter_context(open(f)) for f in files]
    # Все файлы закроются при выходе, даже при исключении
    data = [h.read() for h in handles]

Подводные камни

  • Если __enter__ выбросит исключение, __exit__ не будет вызван — ресурс, захваченный до исключения, останется незакрытым.
  • Возврат True из __exit__ подавляет исключение молча — логируйте это явно, иначе ошибки теряются.
  • В @contextmanager если забыть raise в блоке except, исключение подавляется — поведение отличается от обычного класса менеджера.
  • При вложенных with каждый менеджер получает только исключение, возникшее в его области — внешний менеджер не видит исключений из внутреннего, если они подавлены.
  • BaseException (включая KeyboardInterrupt, SystemExit) тоже передаётся в __exit__ — не заглушайте его случайно.
  • В async-коде нужен async with и __aenter__/__aexit__; синхронный протокол там не работает.

Common mistakes

  • Считать with аналогом только finally без обработки исключений.
  • Не знать параметры exit.
  • Не упоминать ограничение при падении enter.

What the interviewer is testing

  • Объясняет вызов exit при исключении.
  • Понимает suppress semantics.
  • Знает границы гарантии.

Sources

Related topics