【问题标题】:Python __enter__ / __exit__ vs __init__ (or __new__) / __del__Python __enter__ / __exit__ 与 __init__ (或 __new__)/ __del__
【发布时间】:2017-03-25 00:36:35
【问题描述】:

我已经搜索过,但找不到任何充分的理由使用 python 的 __enter__ /__exit__ 而不是 __init__ (或 __new__ ?) / __del__

我了解__enter__ / __exit__ 旨在与with 语句一起用作上下文管理器,with 语句非常棒。但与之对应的是,这些块中的任何代码在该上下文中执行。通过使用这些而不是__init__ / __del__,我似乎正在与调用者创建一个隐式合约,他们必须使用with,但是没有办法强制执行这样的合约,并且该合约只能通过文档(或阅读代码)。这似乎是个坏主意。

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

那么任何人都可以想出一个令人信服的理由来说明我曾经想要使用上下文管理方法而不是构造函数/析构函数方法吗?

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

跟进:

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

【问题讨论】:

标签: python constructor destructor with-statement contextmanager


【解决方案1】:

您似乎忽略了一些差异:

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

  • 不仅会通知上下文管理器上下文结束,还会通知退出是由异常引起的。然后它可以决定处理该事件或在退出期间做出不同的反应。再次以数据库连接为例,根据异常情况,您可以提交或中止事务。

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

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

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

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

此 PEP 向 Python 语言添加了一个新语句“with”,以便可以排除 try/finally 语句的标准用法。

【讨论】:

  • 你的前两点真的是关于with 语句,而不是进入和退出。我同意with 很棒,但在这个问题中,我更关心编写可在with 内部使用的灵活对象的最佳方法。我得到了__del__ 点。
  • @BobDoolittle:实现__enter____exit__ 没有 with 语句几乎没有意义。 with 声明是我们首先拥有这些方法的原因。
  • 当然有。这是服务器和客户端之间的区别。在调用者和服务之间。我写一个对象。其他人可能会使用它。他们可能会在with 块中使用它,我无法强制使用。我应该以在任何一种情况下都能正确执行的方式编写我的对象。就像File 对象一样——我可以在with 语句中使用then,它们都可以使用。但是,在 with 块之外我应该调用 close() (但我怀疑 __del__ 也会这样做)。
  • @BobDoolittle:__enter____exit__ 方法是一个协议。如果您想支持使用您的对象作为上下文管理器,您只会实现这些。否则,您不能将对象用作上下文管理器。如果您不想成为上下文管理器,请不要实现该协议。这与实现映射或容器或数字协议的选择相同。如果您的用例需要,您可以实施这些。
  • @BobDoolittle:您似乎将创建上下文管理器与使用它来管理上下文混为一谈。这两者是独立的步骤。打开文件并同时将其用作上下文管理器通常很方便,但您不必这样做。你为什么要这么做?锁可以一次又一次地用作上下文管理器。有关上下文管理器的更多示例,请参阅Other builtin or practical examples of python `with` statement usage?
【解决方案2】:

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

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

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

【讨论】:

  • 我理解您对__del__ 的看法。但不是__init____init__ 在上下文管理内部或外部应该是可靠的。在这一点上,我认为编写灵活对象的最佳方法是将初始化代码放入__init__,而不是__enter__,并使__exit____del__ 做同样的事情(同时保护反对重复执行)。事实上,我怀疑这正是 File 对象所做的。
  • @BobDoolittle: __init__ 用于初始化,所以如果你想进行初始化,就在那里进行。 __enter__ 用于专门在输入 with 语句时应该发生的工作;例如,__enter__ 可能会锁定一把锁,而 __exit__ 可能会解锁它。
【解决方案3】:

通过使用这些而不是 __init__ / __del__,我似乎正在与调用者创建一个隐式合同,他们必须使用 with,但没有办法强制执行这样的合同

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

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


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

【讨论】:

    猜你喜欢
    • 2010-12-31
    • 2011-06-19
    • 1970-01-01
    • 2019-02-06
    • 2014-10-24
    • 1970-01-01
    • 2016-03-08
    • 2014-04-20
    相关资源
    最近更新 更多