Контекстные менеджеры: __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__вызывается гарантированно — даже когда исключение пролетает сквозь блок, что сейчас и проверим.
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'> поймали снаружи: что-то сломалось
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.
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))
1. Что попадает в переменную x в конструкции with obj as x?
2. Когда вызывается __exit__?
3. Что получают аргументы __exit__, если блок прошёл без ошибок?
4. Что сделает Python, если __exit__ вернёт True?
5. Чем with лучше try/finally для очистки?
Создайте класс Checkout — контекстный менеджер корзины: __enter__ печатает «корзина открыта» и возвращает self, метод add(item) добавляет товар, __exit__ печатает «товаров в корзине: N» по числу позиций. Внутри with добавьте «кофе» и «чай».
Что такое контекстный менеджер в 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
ООП · Урок 13
Перегрузка операторов: __add__ и __getitem__
Плюс склеивает строки, объединяет списки и складывает числа — за каждым знаком стоит магический метод. Научим вектор складываться с вектором, плейлист — отвечать на индексы и срезы, а len() — считать элементы.
ООП · Урок 15
Итераторы: __iter__ и __next__
Каждый for в Python под капотом вызывает iter() и next(), а о конце данных сообщает исключение StopIteration. Научим свои классы работать в цикле: сначала вручную через __iter__ и __next__, потом коротким путём — генератором с yield.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
asyncio · Урок 16
Асинхронные контекст-менеджеры: async with
Соединение надо не только открыть, но и закрыть — даже если посреди работы задачу отменили. async with берёт эту уборку на себя.
async with python контекстasync with python
SQLAlchemy · Урок 20
Проект: менеджер задач на ORM с тестами
Финал курса: собираем менеджер задач — модели со связью, CRUD, доска и тест-сьют, который проверяет всё это шестью зелёными тестами.
менеджер задач sqlalchemy
ООП · Урок 1
Классы в Python: первый объект
Класс — чертёж, объект — экземпляр по этому чертежу. Создаём первый класс, вызываем методы и убеждаемся: в Python объектом является всё, включая функции.
класс python это