在 Python 中依靠 __del__() 进行清理不可靠吗?

Is relying on __del__() for cleanup in Python unreliable?

我在 Python 中阅读了有关清理对象的不同方法,我偶然发现了这些问题 (1, 2),这些问题基本上是说使用 __del__() 进行清理是不可靠的并且应该避免以下代码:

def __init__(self):
    rc.open()

def __del__(self):
    rc.close()

问题是,我使用的正是这段代码,我无法重现上述问题中提到的任何问题。据我所知,我不能选择 with 语句,因为我为闭源软件(testIDEA,任何人?)提供了一个 Python 模块。该软件将创建实例特别是 类 并处理它们,这些实例必须准备好在两者之间提供服务。我看到 __del__() 的唯一替代方法是根据需要手动调用 open()close(),我认为这很容易出错。

我知道当我关闭解释器时,不能保证我的对象会被正确销毁(这并不会打扰我,见鬼,甚至 Python 作者决定没问题)。除此之外,我使用 __del__() 进行清理是在玩火吗?

使用 __del__ 到 运行 代码存在一些问题。

首先,它只有在您积极跟踪引用时才有效,即便如此,也不能保证它会立即 运行 除非您在整个代码中手动启动垃圾回收.我不了解你,但自动垃圾收集在准确跟踪引用方面几乎毁了我。即使您在代码中非常勤奋,您也依赖于 其他使用您的代码的用户 在引用计数方面同样勤奋。

第二,在很多情况下 __del__ 永远不会达到 运行。初始化和创建对象时是否出现异常?解释器退出了吗?某处有循环引用吗?是的,这里有很多可能会出错的地方,而且很少有方法可以干净、一致地处理它。

三,即使它运行,它也不会引发异常,所以你不能像处理其他代码那样处理来自它们的异常。几乎不可能保证来自不同对象的 __del__ 方法会以任何特定顺序 运行。因此,析构函数最常见的用例——清理和删除一堆对象——有点毫无意义,不太可能按计划进行。

如果您真的想要代码 运行,还有更好的机制 -- 上下文管理器、signals/slots、事件等

您观察到垃圾收集语言中终结器的典型问题。 Java有,C#有,它们都提供了类似Pythonwith关键字的基于作用域的清理方法来处理。

主要问题是,垃圾收集器负责清理和销毁对象。在 C++ 中,对象在超出范围时会被销毁,因此您可以使用 RAII 并具有定义明确的语义。在 Python 中,对象超出范围并在 GC 喜欢的情况下继续存在。根据您的 Python 实施,这可能会有所不同。 CPython 及其基于 refcounting 的 GC 是相当良性的(因此你很少看到问题),而 PyPy、IronPython 和 Jython 可能会使对象存活很长时间。

例如:

def bad_code(filename):
    return open(filename, 'r').read()

for i in xrange(10000):
    bad_code('some_file.txt')

bad_code 泄漏文件句柄。在 CPython 中没关系。 refcount 降为零,并立即删除。在 PyPy 或 IronPython 中,您可能会遇到 IOErrors 或类似问题,因为您耗尽了所有可用的文件描述符(在 Unix 上最多 ulimit 或在 Windows 上最多 509 个句柄)。

使用上下文管理器进行基于范围的清理,如果您需要保证清理,with 更可取。您确切地知道您的对象何时会完成。但有时您无法轻松执行这种范围内的清理。那时您可能会使用 __del__atexit 或类似结构来尽最大努力进行清理。不可靠但总比没有好。

您可以通过显式清理或强制执行显式范围来给用户增加负担,或者您可以尝试 __del__ 并偶尔看到一些奇怪的情况(尤其是解释器关闭)。

如果您使用的是 CPython,那么 __del__ 一旦对象的引用计数为零,就会完全可靠且可预测地触发。 https://docs.python.org/3/c-api/intro.html 的文档状态:

When an object’s reference count becomes zero, the object is deallocated. If it contains references to other objects, their reference count is decremented. Those other objects may be deallocated in turn, if this decrement makes their reference count become zero, and so on.

您可以轻松地测试并亲眼看到这种立即清理发生的情况:

>>> class Foo:
...     def __del__(self):
...         print('Bye bye!')
... 
>>> x = Foo()
>>> x = None
Bye bye!
>>> for i in range(5):
...     print(Foo())
... 
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
>>>

(尽管如果您想在 REPL 中测试涉及 __del__ 的东西,请注意最后计算的表达式的结果存储为 _,这算作参考。)

换句话说,如果你的代码在CPython中严格来说是运行,依赖__del__是安全的。