【问题标题】:Is relying on __del__() for cleanup in Python unreliable?在 Python 中依赖 __del__() 进行清理不可靠吗?
【发布时间】:2016-05-31 01:06:43
【问题描述】:

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

def __init__(self):
    rc.open()

def __del__(self):
    rc.close()

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

我明白,当我关闭解释器时,无法保证我的对象会被正确销毁(这并没有给我带来太多困扰,哎呀,即使是 Python 作者也认为没问题)。除此之外,我是不是用__del__()进行清理玩火?

【问题讨论】:

  • 当解释器关闭时,__exit__ 是否也能保证执行...?
  • 另外:我发现this link 在这个问题上很有帮助。
  • @RickTeachey 你把__del__ 误认为__exit__,还是建议我在我的代码中使用__exit__?此外,我可以容忍关闭时出现的问题。
  • 不,我的意思是,既然你说“我明白当我关闭解释器时,不能保证我的对象会被正确销毁......”这似乎暗示@ 987654337@ 将在这种情况下执行。我很想说不会,但意识到我实际上并不知道答案。抱歉 - 这更像是一个侧面评论。
  • @DmitryGrigoryev 答案seems to be NO

标签: python destructor


【解决方案1】:

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

主要问题是,垃圾收集器负责清理和销毁对象。在 C++ 中,对象在超出范围时会被销毁,因此您可以使用 RAII 并具有明确定义的语义。在 Python 中,只要 GC 喜欢,对象就会超出范围并继续存在。根据您的 Python 实现,这可能会有所不同。具有基于引用计数的 GC 的 CPython 相当温和(因此您很少会看到问题),而 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 中,这无关紧要。引用计数降至零,并立即被删除。在 PyPy 或 IronPython 中,您可能会遇到 IOErrors 或类似问题,因为您用尽了所有可用的文件描述符(在 Unix 上最多 ulimit 或在 Windows 上最多 509 个句柄)。

如果您需要保证清理,最好使用上下文管理器和with 进行基于范围的清理。您确切地知道您的对象何时完成。但有时你不能轻易地强制执行这种范围清理。那是您可以使用__del__atexit 或类似结构来尽最大努力进行清理的时候。不可靠,但聊胜于无。

您可以通过显式清理或强制执行显式范围来给用户带来负担,或者您可以与__del__ 赌一把,不时看到一些奇怪的东西(尤其是解释器关闭)。

【讨论】:

  • 谢谢。实际上,我在代码中依赖的不仅仅是尽力而为的清理。您能否阐明 CPython 在您的示例中保证什么?最多只有一个打开的把手?最多10个?还有什么?
  • CPython 使用引用计数系统来处理清理。因此在上面的示例中,句柄超出范围,这会减少引用计数并立即清理,因此在函数运行时只有一个句柄是打开的。
  • 这对我来说已经足够了。我可以摆脱对特定 Python 解释器的要求来运行我的代码。
【解决方案2】:

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

首先,它仅在您积极跟踪引用时才有效,即使那样,也不能保证它会立即运行,除非您在整个代码中手动启动垃圾收集。我不了解你,但是自动垃圾收集在准确跟踪引用方面已经让我很受宠若惊了。即使您对代码非常勤奋,您也依赖于使用您的代码的其他用户,在引用计数方面同样勤奋。

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

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

如果您真的希望代码运行,还有更好的机制——上下文管理器、信号/槽、事件等。

【讨论】:

  • 您能否详细说明 init 中的异常情况?我应该抓住并重新抛出吗?我知道'with'是要走的路,但我必须向闭源调用者提供一个对象,所以我的选择似乎有限
  • 如果您在__init__ 中创建或执行操作,并且在__init__ 期间出现错误,它将无法完成,并且永远不会创建对该类的引用,所以__del__ 永远不会被调用来拆除或清理您在错误发生之前创建的任何内容。您必须提供的对象的规格是什么?你能给我指点封闭源代码库的文档吗?
  • this page 有一些信息,如果你想看看的话。相关部分是Tasks->Writing Script Extensions。
  • @Brendan Abel __init__ 中的错误不会阻止调用 __del__
【解决方案3】:

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

当一个对象的引用计数变为零时,该对象被释放。如果它包含对其他对象的引用,则它们的引用计数会减少。如果这个减量使它们的引用计数变为零,那么这些其他对象可能会被依次释放,等等。

您可以自己轻松测试并看到这种即时清理:

>>> 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__ 是安全的。

【讨论】:

  • 参考周期呢?
  • @Bananach 如果你有引用周期,那么你的对象的引用计数不会下降到零,你需要等待垃圾收集器。
猜你喜欢
  • 2011-07-25
  • 1970-01-01
  • 2013-12-30
  • 1970-01-01
  • 2023-03-27
  • 2011-09-11
  • 2018-06-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多