Перейти к содержанию

252 уроков, 17 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Средний 35 мин 140 XP

Контекстные менеджеры: __enter__ и __exit__

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

Редакция Питоники

Блок with вы использовали с файлами и соединениями: ресурс гарантированно освобождается, даже если внутри блока случилась ошибка. Секрет в двух магических методах — __enter__ и __exit__. Определив их, вы превратите любой свой класс в контекстный менеджер: журнал сессий, транзакцию, блокировку. Это последний dunder-протокол перед итераторами из урока 15, и последний инструмент перегрузки после операторов.

Протокол: __enter__ и __exit__

Конструкция with выражение as переменная работает так: Python вызывает __enter__(), её результат кладёт в переменную, выполняет блок и в конце — при любом исходе — вызывает __exit__. Две частые ошибки: забыть return self в __enter__ (тогда as получит None) и забыть сами методы (тогда with отклонит объект с AttributeError).

журнал сессии как контекстный менеджер
class LogSession:
    def __init__(self, name):
        self.name = name
        self.lines = []

    def __enter__(self):
        print(f"открываю журнал {self.name}")
        return self

    def write(self, text):
        self.lines.append(text)

    def __exit__(self, exc_type, exc_value, traceback):
        print(f"закрываю журнал {self.name}, записей: {len(self.lines)}")

with LogSession("заказ") as log:
    log.write("заказ создан")
    log.write("заказ оплачен")
Вывод
открываю журнал заказ
закрываю журнал заказ, записей: 2
  • exc_type — класс исключения; если блок прошёл без ошибок, None;
  • exc_value — само исключение (у него удобно печатать сообщение);
  • traceback — трассировка, где именно случилась ошибка;
  • __exit__ вызывается гарантированно — даже когда исключение пролетает сквозь блок, что сейчас и проверим.
__exit__ видит исключение, но не глушит его
class GuardedSection:
    def __enter__(self):
        print("входим в секцию")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("тип исключения:", exc_type)
        return False

try:
    with GuardedSection():
        print("внутри блока")
        raise ValueError("что-то сломалось")
except ValueError as error:
    print("поймали снаружи:", error)
Вывод
входим в секцию
внутри блока
тип исключения: <class 'ValueError'>
поймали снаружи: что-то сломалось
что делает возврат True
class SilenceErrors:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("поглотили:", exc_value)
        return True

with SilenceErrors():
    raise ValueError("ошибка, которую никто не увидит")

print("программа продолжает работать")
Вывод
поглотили: ошибка, которую никто не увидит
программа продолжает работать

Гарантия очистки: with вместо try/finally

Тот же эффект раньше писали через try/finally: очистка — в finally, на все ветки. with делает это компактнее и не даёт забыть очистку: она зашита в класс. Классический сценарий — транзакция: успех коммитится, ошибка откатывается; решение принимается прямо в __exit__ по exc_type.

транзакция: commit при успехе, rollback при ошибке
class FakeConnection:
    def commit(self):
        print("commit: изменения закреплены")

    def rollback(self):
        print("rollback: изменения отменены")

class Transaction:
    def __init__(self, connection):
        self.connection = connection

    def __enter__(self):
        return self.connection

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            self.connection.commit()
        else:
            self.connection.rollback()
        return False

connection = FakeConnection()

try:
    with Transaction(connection):
        print("перевод выполняется")
        raise RuntimeError("недостаточно средств")
except RuntimeError as error:
    print("клиенту:", error)
Вывод
перевод выполняется
rollback: изменения отменены
клиенту: недостаточно средств
Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

events = []

class Resource:
    def __enter__(self):
        events.append("взяли")
        return "данные"

    def __exit__(self, exc_type, exc_value, traceback):
        events.append("отдали")

with Resource() as value:
    events.append(value)

print(" -> ".join(events))
events = []

class Swallow:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        events.append("глушим")
        return True

with Swallow():
    events.append("до ошибки")
    1 / 0

events.append("после блока")
print("; ".join(events))
class DB:
    def __init__(self):
        self.log = []

    def commit(self):
        self.log.append("commit")

    def rollback(self):
        self.log.append("rollback")

class Tx:
    def __init__(self, db):
        self.db = db

    def __enter__(self):
        return self.db

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            self.db.commit()
        else:
            self.db.rollback()
        return False

db = DB()
try:
    with Tx(db):
        raise ValueError("нет")
except ValueError:
    pass

with Tx(db):
    db.log.append("запись")

print(", ".join(db.log))
Проверь себя
0 / 5

1. Что попадает в переменную x в конструкции with obj as x?

2. Когда вызывается __exit__?

3. Что получают аргументы __exit__, если блок прошёл без ошибок?

4. Что сделает Python, если __exit__ вернёт True?

5. Чем with лучше try/finally для очистки?

Карточки терминов
Запомнено: 0 / 6
Практика

Создайте класс Checkout — контекстный менеджер корзины: __enter__ печатает «корзина открыта» и возвращает self, метод add(item) добавляет товар, __exit__ печатает «товаров в корзине: N» по числу позиций. Внутри with добавьте «кофе» и «чай».

practice.py
Вопросы и ответы по уроку

Что такое контекстный менеджер в Python простыми словами?

Это объект, который умеет работать в конструкции with: на входе он готовит ресурс (__enter__), на выходе — гарантированно очищает (__exit__), даже если внутри блока возникло исключение. Файлы, блокировки и соединения с базой — всё это контекстные менеджеры.

Урок 13: перегрузка операторов

Когда вызывается __exit__ и что он получает?

Сразу по выходу из блока with — при обычном завершении и при исключении. Аргументы: exc_type (класс исключения), exc_value (само исключение) и traceback; без ошибки все три равны None.

Что будет, если __exit__ вернёт True?

Python сочтёт исключение обработанным и подавит его: программа продолжится так, будто ошибки не было. Это удобный способ гасить конкретные ожидаемые ошибки, но опасный по умолчанию — непредвиденные сбои исчезнут молча. Стандартный вариант — вернуть False или ничего.

Можно ли обойтись без with и написать try/finally?

Можно: with — синтаксический сахар над try/finally с вызовами __enter__/__exit__. Но менеджер держит очистку внутри класса: вызывающий код короче и не может забыть про finally, а несколько ресурсов пишутся одной строкой — with A() as a, B() as b:

Понравился урок? Сошлитесь на него

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.