Вам передали один экземпляр контекстного менеджера для вложенных блоков with: какое свойство его реализации определит, не испортит ли внутреннее состояние внешний выход?
Главное свойство — реентерабельность: способность одного экземпляра корректно поддерживать несколько активных входов, включая вложенные. Если менеджер хранит состояние текущего входа в одном атрибуте, внешний __exit__ может затереть или преждевременно освободить состояние внутреннего блока.
Для вложенного использования состояние каждого входа нужно хранить независимо — обычно в стеке — либо создавать отдельный экземпляр менеджера для каждого блока.
Протокол контекстного менеджера появился в Python как унифицированный способ сопоставить подготовку ресурса с гарантированным завершающим действием. Конструкция with вызывает __enter__, выполняет тело блока, а затем вызывает __exit__, в том числе при исключении.
Сам протокол не делает объект реентерабельным автоматически. Он лишь задаёт порядок вызовов; корректное хранение состояния и возможность повторного или вложенного использования остаются ответственностью реализации класса.
Один экземпляр может использоваться последовательно без проблем: после завершения первого блока его состояние уже сброшено. При вложенном использовании новый __enter__ вызывается до __exit__ внешнего входа, поэтому один и тот же атрибут объекта может быть перезаписан.
В результате внешний выход способен освободить ресурс внутреннего блока, восстановить неправильное значение или выполнить очистку не тем количеством раз. Особенно опасны менеджеры, которые хранят в полях текущий файл, временный каталог, транзакцию или первоначальное значение глобального состояния.
Реентерабельность также не равна потокобезопасности. Стек входов решает вложенность в одном потоке, но для совместного использования экземпляра между потоками могут понадобиться блокировки или отдельное состояние на поток.
Реентерабельный менеджер должен сохранять отдельный кадр состояния для каждого входа. При каждом __enter__ он помещает данные в стек, а соответствующий __exit__ снимает именно верхний кадр.
Внешний __exit__ вызывается после внутреннего и получает собственный кадр, оставшийся в стеке. Если ресурс нельзя безопасно представить как набор независимых кадров, лучше не делать менеджер реентерабельным: явно создавать новый экземпляр на каждый with или запрещать вложенный вход понятным исключением.
При проектировании нужно отдельно определить семантику освобождения ресурса. Для счётного ресурса вложенные входы могут увеличивать счётчик, а финальная очистка выполняться при выходе из последнего уровня; для транзакций вложенный уровень может означать точку сохранения, а не новую транзакцию.
В библиотеке есть менеджер временного изменения настроек приложения. Команда хотела использовать один экземпляр и во внешнем, и во внутреннем with. Простая реализация сохраняла исходное значение в поле self.previous, поэтому внутренний вход перезаписывал его, а внешний выход восстанавливал уже не исходную настройку.
Рассматривались два решения. Запретить повторный вход проще и безопаснее, но это ограничивает композицию кода. Создавать новый объект на каждый блок надёжно, однако требует изменить API и передавать состояние между экземплярами.
Выбрали стек предыдущих значений: каждый вход сохраняет собственную настройку, каждый выход восстанавливает верхнее значение. Это сохранило удобный API и корректность вложенных блоков; для использования между потоками дополнительно ввели отдельное состояние на поток.
Является ли повторное последовательное использование признаком реентерабельности?
Нет. Последовательное использование проверяет, умеет ли объект корректно завершить один цикл и начать следующий. Реентерабельность требует корректно обработать новый вход до завершения предыдущего, то есть вложенные или иным образом перекрывающиеся вызовы.
Достаточно ли заменить поле текущего состояния на счётчик входов?
Нет, если при каждом входе нужно восстановить разные значения. Счётчик подходит для ресурса с простой логикой «создать при глубине ноль, освободить при возврате к нулю», но не сохраняет данные каждого уровня. Для восстановления настроек, дескрипторов или savepoint нужен стек либо другой эквивалентный набор кадров состояния.
Можно ли считать менеджер реентерабельным, если его __exit__ всегда возвращает False?
Нет. Возврат False означает лишь, что исключение из блока не подавляется. Он не говорит ничего о корректности вложенного состояния, порядке освобождения ресурсов или возможности повторного входа. Реентерабельность определяется тем, как __enter__ и __exit__ связывают каждый вход с соответствующим состоянием и очисткой.