Python __enter__/__exit__ против __init__ (или __new__)/__del__

Я искал, и я не могу придумать какой-либо веской причины для использования __enter__ /__exit__ python вместо __init__ (или __new__ ?) / __del__ .

Я понимаю, что __enter__/__exit__ предназначены для использования с оператором with в качестве менеджеров контекста, и оператор with великолепен. Но противоположностью этому является то, что любой код в этих блоках только выполняется в этом контексте. Используя их вместо __init__/__del__, я, кажется, создаю неявный контракт с вызывающими абонентами, что они должны использовать with, но нет никакого способа обеспечить соблюдение такого контракта, и контракт передается только через документацию (или чтение кода). Это кажется плохой идеей.

Кажется, я получаю тот же эффект, используя __init__/__del__ внутри блока with. Но, используя их, а не методы управления контекстом, мой объект полезен и в других сценариях.

Итак, может ли кто-нибудь придумать вескую причину, почему я когда-либо хочу использовать методы управления контекстом, а не методы конструктора/деструктора?

Если есть лучшее место, чтобы задать подобный вопрос, пожалуйста, дайте мне знать, но, похоже, там не так много хорошей информации об этом.

Последующие действия:

Этот вопрос был основан на неверном (но, вероятно, распространенном) предположении, поскольку я всегда использовал with для создания экземпляра нового объекта, и в этом случае __init__/__del__ очень близко к тому же поведению, что и __enter__/__exit__ (за исключением того, что вы не можете контролировать, когда или если __del__ будет выполняться, это зависит от сборки мусора, и если процесс завершится первым, он никогда не будет вызван). Но если вы используете уже существующие объекты в операторах with, они, конечно, сильно отличаются.


person BobDoolittle    schedule 10.11.2016    source источник
comment
Когда (и даже будет ли) вызываться __del__, это недетерминировано. Вы можете потерять данные, полагаясь на __del__ для очистки.   -  person user2357112 supports Monica    schedule 10.11.2016
comment
возможный обман: stackoverflow.com/a/6772907/674039   -  person wim    schedule 11.11.2016


Ответы (3)


Есть несколько отличий, которые вы, кажется, пропустили:

  • Менеджер контекста получает возможность предоставить новый объект только для блока, который вы выполняете. Некоторые менеджеры контекста просто возвращают туда self (как это делают файловые объекты), но, например, объекты соединения с базой данных могут возвращать объект-курсор, привязанный к текущей транзакции.

  • Менеджеры контекста уведомляются не только об окончании контекста, но и о том, был ли выход вызван исключением. Затем он может принять решение об обработке этого события или иначе реагировать во время выхода. Снова используя соединение с базой данных в качестве примера, на основе исключения вы можете либо зафиксировать, либо прервать транзакцию.

  • __del__ вызывается только при удалении всех ссылок на объект. Это означает, что вы не можете полагаться на его вызов, если вам нужно иметь несколько ссылок на него, время жизни которых вы можете или не можете контролировать. Однако выход диспетчера контекста точно определен.

  • Менеджеры контекста можно повторно использовать, и они могут сохранять состояние. Снова соединение с базой данных; вы создаете его один раз, затем снова и снова используете его в качестве менеджера контекста, и он будет держать это соединение открытым. Для этого нет необходимости каждый раз создавать новый объект.

    Это важно, например, для блокировок потоков; вы должны сохранять состояние, чтобы только один поток мог одновременно удерживать блокировку. Вы делаете это, создавая один объект блокировки, а затем используете with lock:, чтобы разные потоки, выполняющие этот раздел, каждый мог ожидать, прежде чем войти в этот контекст.

Методы __enter__ и __exit__ формируют протокол диспетчера контекста, и их следует использовать только в том случае, если вы действительно хотите управлять контекстом. Целью менеджеров контекста является упрощение общих шаблонов try...finally и try...except, а не управление временем жизни одного экземпляра. См. PEP 343 — Утверждение with. :

Этот PEP добавляет новый оператор with в язык Python, чтобы можно было исключить стандартное использование операторов try/finally.

person Martijn Pieters    schedule 10.11.2016
comment
Ваши первые два пункта действительно касаются оператора with, а не входа и выхода. Я согласен, что with великолепен, но в этом вопросе меня больше беспокоит лучший способ написать гибкий объект, который можно использовать внутри with или нет. Я получаю __del__ балл. - person BobDoolittle; 11.11.2016
comment
@BobDoolittle: нет смысла реализовывать __enter__ и __exit__ без оператора with. Оператор with — это причина, по которой у нас есть эти методы. - person Martijn Pieters; 11.11.2016
comment
Конечно, есть. Это разница между сервером и клиентом. Между абонентом и сервисом. Я пишу объект. Кто-то другой может использовать его. Они могут использовать его в блоке with или нет, и у меня нет возможности принудительно использовать его. Я должен написать свой объект таким образом, чтобы он работал правильно в любом случае. Как и объекты File — я могу использовать then в операторе with или нет, и они работают в любом случае. Однако за пределами блока with я должен вызывать close() (но я подозреваю, что __del__ делает то же самое). - person BobDoolittle; 11.11.2016
comment
@BobDoolittle: методы __enter__ и __exit__ представляют собой протокол. Вы бы реализовали их только в том случае, если бы хотели поддерживать использование вашего объекта в качестве менеджера контекста. В противном случае вы не можете использовать объект в качестве менеджера контекста. Не реализуйте протокол, если вы не хотите быть менеджером контекста. Это тот же выбор, что и реализация картографических, контейнерных или числовых протоколов. Вы бы реализовали их, если ваш вариант использования требует этого. - person Martijn Pieters; 11.11.2016
comment
В этом случае должно быть какое-то соблюдение языка - например. генерировать исключение при создании экземпляра такого объекта за пределами with. Я снова возвращаюсь к примеру с объектом File. Это не заставляет вас использовать with. Вы можете напрямую создать экземпляр и использовать его. Но в этом случае вам, вероятно, следует быть более осторожным (например, явно вызывать close()). Чтобы создать такое поведение, он не должен полагаться на __enter__. - person BobDoolittle; 11.11.2016
comment
@BobDoolittle: Зачем это нужно? Существует исключение, когда вы хотите использовать объект в операторе with, который не является менеджером контекста. Вам не обязательно создавать диспетчер контекста в операторе with. lock = threading.Lock() подходит, тогда with lock: позволяет управлять блокировкой контекста. - person Martijn Pieters; 11.11.2016
comment
@BobDoolittle: Кажется, вы путаете создание диспетчера контекста с его использованием для управления контекстом. Это два отдельных шага. Часто бывает удобно открыть файл и одновременно использовать его в качестве контекстного менеджера, но это не обязательно. Зачем тебе это нужно? Блокировки можно использовать в качестве менеджера контекста снова и снова. См. Другие встроенные или практические примеры использования инструкции Python `with`? для получения дополнительных примеров менеджеров контекста. - person Martijn Pieters; 11.11.2016
comment
Да ты прав. Я привык создавать новый объект в операторе with, но это не единственный шаблон использования. Имеет смысл, спасибо. - person BobDoolittle; 11.11.2016

del x не вызывает напрямую x.__del__()

Вы не можете контролировать, когда вызывается .__del__, или фактически будет ли он вызываться вообще.

Поэтому использование __init__/__del__ для управления контекстом ненадежно.

person wim    schedule 10.11.2016
comment
Я понимаю вашу точку зрения по поводу __del__. Но не __init__. __init__ должен быть надежным внутри или вне управления контекстом. На данный момент я думаю, что лучший способ написать гибкий объект — поместить код инициализации в __init__, а не __enter__, и заставить __exit__ делать то же самое, что и __del__ (при этом защищая от дублирования выполнения). На самом деле, я подозреваю, что именно это и делают объекты File. - person BobDoolittle; 11.11.2016
comment
@BobDoolittle: __init__ предназначен для инициализации, поэтому, если вы хотите выполнить инициализацию, сделайте это здесь. __enter__ предназначен для работы, которая должна выполняться непосредственно при вводе оператора with; например, __enter__ может заблокировать замок и __exit__ разблокировать его. - person user2357112 supports Monica; 11.11.2016

Используя их вместо __init__ / __del__, я, кажется, создаю неявный контракт с вызывающими абонентами, что они должны использовать with, но нет никакого способа обеспечить соблюдение такого контракта.

У вас есть контракт в любом случае. Если пользователи используют ваш объект, не осознавая, что он требует очистки после использования, они все испортят, независимо от того, как вы реализуете очистку. Они могут хранить ссылку на ваш объект навсегда, например, предотвращая запуск __del__.

Если у вас есть объект, который требует специальной очистки, вам нужно сделать это требование явным. Вам нужно предоставить пользователям with функциональные возможности и явный close или аналогичный метод, чтобы пользователи могли контролировать, когда происходит очистка. Вы не можете скрыть требование очистки внутри метода __del__. Возможно, вы также захотите реализовать __del__ в качестве меры безопасности, но вы не можете использовать __del__ вместо with или явное close.


При этом Python не обещает, что __del__ когда-либо будет работать. Стандартная реализация запустит __del__, когда счетчик ссылок объекта упадет до 0, но этого может не произойти, если ссылка сохраняется до конца скрипта или если объект находится в цикле ссылок. Другие реализации не используют подсчет ссылок, что делает __del__ еще менее предсказуемым.

person user2357112 supports Monica    schedule 10.11.2016