Python __enter__ / __exit__ 对比 __init__(或 __new__)/ __del__

Python __enter__ / __exit__ vs __init__ (or __new__) / __del__

我已经搜索过了,但我无法想出任何好的理由来使用 python 的 __enter__ /__exit__ 而不是 __init__(或 __new__ ?) / __del__ .

我知道 __enter__ / __exit__ 旨在与 with 语句一起用作上下文管理器,并且 with 语句很棒。但与此对应的是,这些块中的任何代码 在该上下文中执行。通过使用这些而不是 __init__ / __del__ 我似乎正在与调用者创建一个隐含的合同,他们必须使用 with,但是没有办法执行这样的合同,合同是仅通过文档(或阅读代码)进行交流。这似乎是个坏主意。

我似乎在 with 块中使用 __init__ / __del__ 得到了相同的效果。但是通过使用它们而不是上下文管理方法,我的对象在其他场景中也很有用。

所以有人能想出一个令人信服的理由来说明我 曾经 想要使用上下文管理方法而不是 constructor/destructor 方法吗?

如果有更好的地方可以提出这样的问题,请告诉我,但似乎没有太多关于此的有用信息。

跟进:

这个问题是基于一个错误的(但可能很常见)假设,因为我总是使用 with 来实例化一个新对象,在这种情况下 __init__/__del__ 非常接近与 __enter__/__exit__ 相同的行为(除了你无法控制何时或是否执行 __del__,这取决于垃圾收集,如果进程先终止,它可能永远不会被调用)。但是,如果您在 with 语句中使用预先存在的对象,它们当然会完全不同。

您似乎忽略了几个不同之处:

  • 上下文管理器有机会为您正在执行的块提供一个新对象。一些上下文管理器只是 return self 那里(就像文件对象一样),但是,例如,数据库连接对象可以 return 一个与当前事务相关联的游标对象。

  • 上下文管理器不仅会收到上下文结束的通知,还会收到异常导致退出的通知。然后它可以决定处理该事件或在退出期间以其他方式做出不同的反应。再次以数据库连接为例,基于异常,您可以提交或中止事务。

  • __del__ 仅在 所有 对对象的引用被删除时调用。这意味着如果您需要对它的多个引用而您可能会或可能不会控制其生命周期,则不能依赖它被调用。然而,精确定义了上下文管理器出口。

  • 上下文管理器可以重用,并且它们可以保持状态。再次连接数据库;您创建它一次,然后一次又一次地将它用作上下文管理器,它会保持该连接打开。不需要为此每次都创建一个新对象。

    例如,这对于线程锁很重要;您必须 保持状态以便一次只有一个线程可以持有锁。为此,您可以创建 one 锁定对象,然后使用 with lock:,这样执行该部分的不同线程都可以在进入该上下文之前等待。

__enter____exit__ 方法构成了 上下文管理器协议 ,只有在您确实想要管理上下文时才应使用这些方法。上下文管理器的目标是简化常见的 try...finallytry...except 模式,而不是管理单个实例的生命周期。见 PEP 343 – The "with" Statement:

This PEP adds a new statement "with" to the Python language to make it possible to factor out standard uses of try/finally statements.

del x 不直接调用 x.__del__()

您无法控制何时调用 .__del__,或者实际上调用 whether it gets called at all

因此,使用__init__/__del__进行上下文管理是不靠谱的。

By using these instead of __init__ / __del__ I appear to be creating an implicit contract with callers that they must use with, yet there's no way to enforce such a contract

无论哪种方式,您都有合同。如果用户在没有意识到使用后需要清理的情况下使用您的对象,那么无论您如何实现清理,他们都会把事情搞砸。他们可能会永远保留对您的对象的引用,例如,阻止 __del__ 从 运行ning.

如果您有需要特殊清理的对象,则需要明确此要求。您需要为用户提供 with 功能和明确的 close 或类似方法,让用户控制何时进行清理。您不能将清理要求隐藏在 __del__ 方法中。作为一项安全措施,您可能也想实施 __del__,但您不能使用 __del__ 代替 with 或明确的 close.


话虽如此,Python 不承诺 __del__ 将永远 运行。当对象的引用计数下降到 0 时,标准实现将 运行 __del__,但如果引用在脚本末尾仍然存在,或者对象处于引用循环中,则可能不会发生这种情况。其他实现不使用引用计数,使得 __del__ 更不可预测。