__del__的使用场景

16

在Python 3中,编写自定义__del__方法或依赖于stdlib中的1之一,有哪些用例?也就是说,在什么情况下使用它是相当安全的,并且可以做一些难以在没有它的情况下完成的事情?

出于许多很好的原因(123456),通常的建议是避免使用__del__而是使用上下文管理器或手动清理:

  1. 如果对象在解释器退出时仍存活,那么不能保证会调用__del__2
  2. 当一个对象可以被销毁的时候,引用计数实际上可能是非零的(例如,引用可能通过由调用函数持有的回溯帧幸存下来)。这使得销毁时间比gc的简单不可预测性更加不确定。
  3. 如果循环包括多个具有__del__的对象,则垃圾回收器无法清除它们
  4. __del__内部的代码必须非常小心地编写:
    • 因为__init__可能会引发异常,所以在__init__中设置的对象属性可能不存在;
    • 异常被忽略(只打印到stderr)
    • 全局变量可能不再可用。

更新:

PEP 442__del__ 的行为进行了显著改进。不过似乎我的1-4观点仍然成立?


更新2:

一些顶级Python库采用了在后-PEP 442 python(即python 3.4+)中使用__del__的方式。我想PEP 442之后我的第3个观点已不再成立,而其他观点则被接受为对象终止复杂性不可避免。


1我将问题从编写自定义__del__方法扩展到包括依赖于stdlib的__del__

2似乎在较新版本的Cpython中总是在解释器退出时调用__del__(有没有反例?)。但是,对于__del__的可用性来说并不重要:文档明确地没有提供关于此行为的任何保证,因此不能依赖它(它可能在未来版本中发生变化,在非CPython解释器中可能不同)。


1
为什么你在这个问题中排除了标准库? - user1781434
1
因为我在我的代码中(过度)使用了__del__,并且我计划完全放弃它。在我这样做之前,我想确保我没有错过任何不应该避免它的情况。由于我(以及大多数SO上的人)不贡献于stdlib,我希望通过排除它来简化问题。 - max
@ali_m 我也在考虑这个问题。假设您的对象生命周期过于开放,无法封闭在上下文管理器中,因此您最终依赖于 __del__。您必须足够关心清理才会费心编写 __del__。但是,如果清理偶尔从未发生或比预期晚得多(因为这是 __del__ 的语义),您并不介意。什么样的清理工作适合呢?也许是释放内存,但这已经由垃圾回收处理了。也许可以帮助垃圾回收器在内存不足时释放内存? - max
在某些情况下,只有在接近耗尽内存时才需要__del__调用,大多数Python实现在内存变得“稀疏”时收集对象。因此,您不关心“立即清理”,而只关心“最终清理”。 - MSeifert
@max,我并不反对你更改接受的答案(我喜欢新的接受答案-其实我在现在已删除的评论中鼓励了这个答案!),但这是一个非常有趣的话题,你可能想考虑让它保持未被接受状态多几天,因为“有一个被接受的答案的问题不像没有被接受答案的问题那样容易得到更多的关注。”(https://meta.stackexchange.com/a/5235/317868)如果有其他有趣的用例,我会非常感兴趣。最终决定取决于你,我只是希望有更多的回答 :) - MSeifert
显示剩余2条评论
3个回答

10

上下文管理器(以及 try/finally 块)比 __del__ 更加严格限制。通常情况下,它们要求您以一种方式组织代码,使您需要释放的资源的生命周期不会超过调用堆栈中某个级别的单个函数调用,而不是将其绑定到可能在不可预测的时间和地点被销毁的类实例的生命周期之上。通常情况下,将资源的生命周期限制为一个范围是件好事,但有时候这种模式并不适合某些特殊情况。

我使用 __del__ 的唯一情况(除了用于调试,参见 @MSeifert 的回答)是为了释放由外部库分配的内存。由于我正在封装的库的设计,难以避免拥有大量对象,它们持有指向堆分配内存的指针。使用 __del__ 方法来释放指针是进行清理的最简单方法,因为在上下文管理器中包含每个实例的寿命将是不切实际的。


你的 __del__ 做了什么普通垃圾回收无法完成的任务? - max
@max 它释放了在 Python 之外分配的内存(由我包装的库中的 C 函数分配),因此对于 gc 是不可见的。 - ali_m
1
尽管两者都很有帮助,但我会接受你的答案,因为我认为释放malloc分配的内存是一个完美的生产用例。从我的问题来看,第(1)点是无关紧要的,因为进程在退出时会自动释放内存;第(2)点不太相关,因为只要gc在内存完全耗尽之前将其释放,就足够了;第(3)和(4)点只是需要实现时小心谨慎的注意事项。 - max
1
弱引用回调函数是否适合此目的,而不会有__del__的缺点? - Marius Gedminas
1
@MariusGedminas 我问了一个关于这个问题的问题(https://dev59.com/Q1cP5IYBdhLWcg3w19bz),因为我认为在这篇帖子中添加更多的副问题太多了。 - max
显示剩余2条评论

8
一个用例是调试。如果您想要跟踪特定对象的生命周期,编写一个临时__del__方法非常方便。它可以用于记录一些日志或只是print一些内容。我有几次使用过它,特别是当我对实例何时以及如何被删除感兴趣时。有时候知道何时创建和丢弃许多临时实例是很好的。但正如我所说,我只在满足我的好奇心或调试时使用它。
另一个用例是子类化定义了__del__方法的类。有时您会发现一个您想要子类化的类,但内部需要您实际上覆盖__del__来控制实例清除的顺序。这也非常罕见,因为您需要找到一个带有__del__的类,您需要对其进行子类化,并且您需要引入一些内部,这些内部实际上需要在恰好正确的时间调用超类的__del__。我确实做过一次,但我不记得在哪里以及为什么它很重要(也许那时我甚至不知道有其他选择,所以将其视为可能的用例)。
当你包装一个外部对象(例如一个不被Python跟踪的对象),它真的,真的需要被释放,即使有人“忘记”(我怀疑很多人故意省略它们!)使用你提供的上下文管理器。
然而,所有这些情况都应该非常非常罕见(或者应该是这样)。实际上,这有点像元类:它们很有趣,真的很酷,因为你可以探索 Python 的“有趣部分”的概念。但在实践中:

如果你想知道是否需要它们[元类],那么你不需要(实际上需要它们的人确切知道他们需要它们,并且不需要关于为什么需要的解释)。

引用(可能)来自 Tim Peters(我没有找到原始参考文献)。


阻止析构函数在生产代码中有用的主要问题似乎是它们何时或是否被调用的不可预测性。元类虽然稍微复杂但完全可预测,因此更加有用! - max
@max 是的,我想比较毕竟不是那么有用。我只是在考虑使用情况:两者都有使用情况,但它们往往非常稀少(而且非常复杂)。 - MSeifert
顺便提一下,另一种帮助跟踪对象生命周期的技术(我刚刚发现)是设置 PYTHONVERBOSE=2 - max
@max 这不仅仅是关于模块吗?我还没有真正使用过它,我一定会尝试一下 :) 但在我的调试场景中,我只对一个或两个类感兴趣,所以没有必要跟踪所有内容。 :) - MSeifert
啊是的,你说得对,它只包括模块清理信息。 - max

1

我经常使用__del__来关闭aiohttp.ClientSession对象。

如果不这样做,aiohttp将会打印有关未关闭客户端会话的警告。


1
但这样做不可靠,因为根据Python文档,您的__del__可能永远不会被调用。 - max

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接