Почему в Python используют контекстные менеджеры вместо конструкторов и деструкторов для управления ресурсами
Что проверяют
Понимаете ли вы, что в Python нет гарантированного деструктора, и отличаете ли управление памятью от управления ресурсами.
Как отвечать
Конструктор и деструктор задают время жизни объекта, а нужно детерминированное время жизни ресурса: дескриптора, блокировки, сокета, транзакции. В CPython метод __del__ вызывается при обнулении счётчика ссылок и создаёт иллюзию, что этого достаточно. Полагаться на него нельзя:
- подсчёт ссылок это деталь реализации CPython, в других реализациях объект соберётся когда угодно;
- ссылочный цикл откладывает вызов до сборщика поколений;
- пойманное исключение держит кадр стека через трассировку, и локальные переменные живут дольше блока;
- исключение внутри
__del__не пробрасывается наружу, а печатается и игнорируется.
Конструкция with снимает вопрос: __enter__ захватывает ресурс, __exit__ вызывается при любом выходе из блока, включая исключение, return и break. При этом __exit__ получает тип исключения, само исключение и трассировку, то есть может залогировать, откатить транзакцию или подавить исключение, вернув True.
Свой менеджер проще всего написать декоратором:
from contextlib import contextmanager
@contextmanager
def cursor(conn):
cur = conn.cursor()
try:
yield cur
finally:
cur.close()Стоит упомянуть contextlib.ExitStack для набора ресурсов, известного только в рантайме, и async with с парой __aenter__ и __aexit__ для асинхронного кода.
Где спотыкаются
Говорят, что with спасает от утечек памяти. Память тут ни при чём, объект живёт по обычным правилам. Гарантия ровно одна: __exit__ будет вызван, то есть дескриптор закроется, а блокировка отпустится.
Второе: думают, что with обрабатывает исключения. Он их не глотает: __exit__ отработает, и исключение полетит дальше, если явно не вернуть True. Чужой менеджер, который возвращает True, потом ищут часами.
Третье: выносят наружу то, что закрылось на выходе. Возвращают из функции сам файловый объект или ленивый генератор поверх него: к моменту чтения блок давно закончился, и приходит ValueError про операцию с закрытым файлом.