Найдите причину вывода класс вместо экземпляр при входе в контекст:
class Resource:
def __enter__(self):
print("класс")
return self
def __exit__(self, exc_type, exc, tb):
return False
r = Resource()
r.__enter__ = lambda: print("экземпляр")
with r:
pass
Будет выведено класс, потому что неявный вызов методов протокола with выполняется через тип объекта, а не через его словарь атрибутов. Присваивание r.__enter__ создаёт атрибут только у экземпляра, но не заменяет специальный метод Resource.__enter__ для конструкции with.
Протокол контекстного менеджера появился для стандартизации операций подготовки и гарантированного освобождения ресурсов вокруг блока кода. Конструкция with избавляет от ручного шаблона try/finally, но при этом должна надёжно находить служебные методы даже в ситуациях, когда обычный доступ к атрибуту экземпляра переопределён.
Поэтому специальные методы Python, включая __enter__ и __exit__, вызываются по правилам неявного поиска специальных методов. Это общее правило языка используется также для операторов, итерации, вызова объекта и других протоколов.
Обычный атрибут экземпляра можно заменить динамически:
Но это не означает, что таким способом можно изменить поведение всех синтаксических конструкций Python. Если разработчик рассчитывает настроить контекстный менеджер только присваиванием __enter__ конкретному объекту, фактическая логика входа в контекст останется прежней.
Это особенно опасно при тестировании, monkey patching и создании объектов с динамическим поведением: прямой вызов r.__enter__() и конструкция with r могут привести к разным результатам.
Для with r Python концептуально ищет методы контекстного протокола у типа объекта и вызывает их примерно так:
Это не буквальная реализация всех деталей байткода, но она верно отражает принцип поиска. В примере r.__enter__ действительно существует и при прямом обращении вернёт lambda, однако with не использует этот экземплярный атрибут.
Чтобы изменить поведение, метод нужно определить в классе или создать отдельный класс:
Причина такого правила — предсказуемость специальных операций и эффективность их выполнения. Компромисс состоит в том, что гибкость изменения поведения отдельного экземпляра уступает место единообразному протоколу, основанному на типе.
В тесте нужно было проверить обработку ошибки при входе в ресурс, поэтому разработчик присвоил одному экземпляру собственный __enter__. Прямой вызов метода показывал тестовую реализацию, но код приложения с with resource продолжал выполнять настоящий вход в ресурс.
Рассматривались два варианта. Monkey patch класса был простым, но влиял на все экземпляры и мог вызвать взаимное влияние тестов. Создание небольшого тестового подкласса требовало чуть больше кода, зато ограничивало изменение нужным объектом и корректно работало с with.
Выбран был подкласс или фабрика тестовых контекстных менеджеров. Это обеспечило совпадение поведения при прямом вызове и при использовании протокола with, а после теста не требовало восстанавливать состояние общего класса.
with для объекта, у которого __enter__ задан только в __dict__ экземпляра?Нет. Для неявного протокола with наличие __enter__ только у экземпляра недостаточно; метод должен быть доступен через класс объекта. Поэтому объект без __enter__ в классе получит ошибку при использовании в with, даже если экземпляру динамически присвоен атрибут с таким именем.
r.__enter__() может отличаться от поведения with r?Прямой вызов использует обычный поиск атрибута: сначала проверяются атрибуты экземпляра, затем класса. Конструкция with использует специальный неявный поиск через тип, поэтому экземплярное переопределение игнорируется. Это различие важно учитывать при отладке и при подмене поведения в тестах.
__enter__ в самом классе после создания экземпляра?Поведение with изменится и для уже созданного экземпляра, поскольку при входе метод снова будет найден через его текущий тип. Однако такое изменение затронет все экземпляры этого класса, что может привести к утечке состояния между тестами или компонентами приложения. Безопаснее использовать отдельный подкласс либо временно менять атрибут класса с гарантированным восстановлением в finally.