Контекстный менеджер — это объект, который гарантирует выполнение кода очистки (закрытие файла, освобождение блокировки, откат транзакции) независимо от того, завершился ли блок кода нормально или через исключение. В Python он используется через конструкцию with, самый частый пример — работа с файлами: with open('file.txt') as f: гарантирует, что файл закроется при выходе из блока, даже если внутри блока произошла ошибка.
Проблема, которую решает with
f = open('data.txt')
data = f.read()
process(data) # если здесь исключение — f.close() не вызовется
f.close()
data = f.read()
process(data) # если здесь исключение — f.close() не вызовется
f.close()
Без with закрытие файла нужно явно прописывать после работы с ним — но если между открытием и закрытием возникнет исключение, строка f.close() просто не выполнится, и файловый дескриптор останется открытым. При редких запусках это не страшно, но в долго работающем приложении, обрабатывающем много файлов, такие утечки дескрипторов накапливаются и в какой-то момент упираются в лимит открытых файлов операционной системы.
with гарантирует закрытие даже при исключении
with open('data.txt') as f:
data = f.read()
process(data) # если здесь исключение — файл всё равно закроется
data = f.read()
process(data) # если здесь исключение — файл всё равно закроется
with open(...) as f делает то же самое, что try/finally, только короче: файл гарантированно закрывается при выходе из блока with — неважно, дошло выполнение до конца блока нормально или прервалось исключением. Эквивалентная запись через try/finally была бы длиннее и легче забыть, особенно если открывается сразу несколько ресурсов.
Как устроен протокол контекстного менеджера
class DatabaseConnection:
def __enter__(self):
print('Открываю соединение с БД')
self.conn = connect_to_db()
return self.conn
def __exit__(self, exc_type, exc_value, traceback):
print('Закрываю соединение с БД')
self.conn.close()
return False # не подавлять исключение, если оно было
with DatabaseConnection() as conn:
conn.execute('SELECT * FROM users')
def __enter__(self):
print('Открываю соединение с БД')
self.conn = connect_to_db()
return self.conn
def __exit__(self, exc_type, exc_value, traceback):
print('Закрываю соединение с БД')
self.conn.close()
return False # не подавлять исключение, если оно было
with DatabaseConnection() as conn:
conn.execute('SELECT * FROM users')
Любой объект, у которого определены методы __enter__ и __exit__, можно использовать после with. __enter__() выполняется при входе в блок и его возвращаемое значение попадает в переменную после as. __exit__(exc_type, exc_value, traceback) выполняется при выходе из блока в любом случае — три аргумента содержат информацию об исключении, если оно произошло (все три None, если исключений не было). Возврат False (или ничего, что тоже воспринимается как False) из __exit__ означает «не глотать исключение» — оно продолжит распространяться дальше как обычно. Возврат True подавил бы исключение — это редкий и обычно нежелательный паттерн, если не сделан осознанно.
contextlib.contextmanager — короткий способ без класса
from contextlib import contextmanager
@contextmanager
def db_connection():
print('Открываю соединение с БД')
conn = connect_to_db()
try:
yield conn
finally:
print('Закрываю соединение с БД')
conn.close()
with db_connection() as conn:
conn.execute('SELECT * FROM users')
@contextmanager
def db_connection():
print('Открываю соединение с БД')
conn = connect_to_db()
try:
yield conn
finally:
print('Закрываю соединение с БД')
conn.close()
with db_connection() as conn:
conn.execute('SELECT * FROM users')
Писать полноценный класс с __enter__/__exit__ ради простого контекстного менеджера часто избыточно. Декоратор @contextmanager позволяет описать ту же логику одной функцией-генератором: код до yield — это то, что выполнилось бы в __enter__, значение после yield — то, что попадёт в переменную после as, а код в finally после yield — то, что выполнилось бы в __exit__. try/finally внутри такой функции обязателен: без него код очистки не выполнится при исключении внутри блока with.
Несколько контекстных менеджеров одновременно
with open('input.txt') as fin, open('output.txt', 'w') as fout:
fout.write(fin.read().upper())
fout.write(fin.read().upper())
Через запятую можно открыть сразу несколько ресурсов в одном блоке with — оба файла корректно закроются при выходе, даже если исключение произойдёт при работе со вторым файлом уже после открытия первого. Это эквивалентно вложенным with, но короче и читается яснее для двух-трёх ресурсов.
Частые вопросы про контекстные менеджеры в Python
Чем with отличается от try/finally? По сути ничем — with делает то же самое, что try/finally, но короче и с меньшим риском забыть код очистки. Внутри contextlib.contextmanager try/finally используется явно.
Зачем нужны три аргумента у __exit__, если можно просто написать finally? Аргументы exc_type, exc_value, traceback дают доступ к информации об исключении внутри самого __exit__ — например, чтобы залогировать конкретную ошибку или решить, подавлять её или нет, в зависимости от типа. Обычный finally такой информации не даёт напрямую.
Что будет, если __exit__ вернёт True? Исключение, возникшее внутри блока with, будет подавлено — код продолжит выполняться дальше как ни в чём не бывало, без пробрасывания ошибки наверх. Это осознанный паттерн для специфичных случаев, а не поведение по умолчанию, и его легко использовать по ошибке, случайно проглотив важную ошибку.
Когда использовать класс с __enter__/__exit__, а когда @contextmanager? Класс уместен, когда контекстный менеджер сложный, хранит состояние между вызовами или должен интегрироваться с остальным ООП-кодом. @contextmanager короче и удобнее для простых случаев, укладывающихся в одну функцию.
Зачем нужны три аргумента у __exit__, если можно просто написать finally? Аргументы exc_type, exc_value, traceback дают доступ к информации об исключении внутри самого __exit__ — например, чтобы залогировать конкретную ошибку или решить, подавлять её или нет, в зависимости от типа. Обычный finally такой информации не даёт напрямую.
Что будет, если __exit__ вернёт True? Исключение, возникшее внутри блока with, будет подавлено — код продолжит выполняться дальше как ни в чём не бывало, без пробрасывания ошибки наверх. Это осознанный паттерн для специфичных случаев, а не поведение по умолчанию, и его легко использовать по ошибке, случайно проглотив важную ошибку.
Когда использовать класс с __enter__/__exit__, а когда @contextmanager? Класс уместен, когда контекстный менеджер сложный, хранит состояние между вызовами или должен интегрироваться с остальным ООП-кодом. @contextmanager короче и удобнее для простых случаев, укладывающихся в одну функцию.
Закрепить контекстные менеджеры на практике
Написать свой @contextmanager и свой класс с __enter__/__exit__ — быстрее укладывается в голове, чем чтение про протокол. Потренироваться можно на тренажёре Python Arena от Quality Academy — 1339 задач, значительная часть бесплатно.
-------
Полезные ссылки школы
Сайт Quality Academy:
/main
Telegram-канал:
https://t.me/quality_academy
YouTube-канал:
https://www.youtube.com/@quality_academy
ВКонтакте:
https://vk.com/quality_academy
Канал отзывов учеников (58+ отзывов):
https://t.me/quality_academy_reviews
Задать вопрос менеджеру:
https://t.me/quality_academy_bot
Тест «Подойдёт ли вам тестирование»:
https://quiz.quality-academy.ru
5000+ вопросов с собесов тестировщика — бот-тренажёр:
https://t.me/quality_academy_interview_bot
3 практические задачи и дорожная карта:
https://t.me/quality_academy_tasks_bot
Бесплатные тренажёры — SQL Arena, Playwright Arena, Python Arena:
SQL Arena · Playwright Arena · Python Arena
/main
Telegram-канал:
https://t.me/quality_academy
YouTube-канал:
https://www.youtube.com/@quality_academy
ВКонтакте:
https://vk.com/quality_academy
Канал отзывов учеников (58+ отзывов):
https://t.me/quality_academy_reviews
Задать вопрос менеджеру:
https://t.me/quality_academy_bot
Тест «Подойдёт ли вам тестирование»:
https://quiz.quality-academy.ru
5000+ вопросов с собесов тестировщика — бот-тренажёр:
https://t.me/quality_academy_interview_bot
3 практические задачи и дорожная карта:
https://t.me/quality_academy_tasks_bot
Бесплатные тренажёры — SQL Arena, Playwright Arena, Python Arena:
SQL Arena · Playwright Arena · Python Arena