【问题标题】:Why do Python io streams not close on __del__? [duplicate]为什么 Python io 流不会在 __del__ 上关闭? [复制]
【发布时间】:2020-05-30 06:39:27
【问题描述】:

到目前为止,我一直在 Python 开发中犯了一个严重的错误:我一直假设流在其对应的对象超出范围时就会关闭。具体来说,我假设当一个文件或继承io.IOBase 的类的某个实例调用__del__ 时,它将运行对象的close 方法。但是,在执行以下代码后,我注意到情况肯定不是这样。

def wrap_close(old_close):
    def new_close(*args, **kwargs):
        print("closing")
        return old_close(*args, **kwargs)
    return new_close

f = open("tmp.txt")
f.close = wrap_close(f.close)
f.close() # prints "closing"

f = open("tmp.txt")
f.close = wrap_close(f.close)
del(f) # nothing printed

我的问题是,在调用 __del__ 方法时不自动关闭文件或流有什么好处?看起来实现起来很简单,但我想必须有理由允许流在其相应对象超出范围后保持打开状态。

【问题讨论】:

  • 问题是关闭文件描述符是操作系统级别的任务,只有在调用.close() 时才能正确完成。 del 没有内置任何特殊行为来处理流 - 它只是从命名空间中删除对象,然后垃圾收集器将其堆肥。他们都不关心某处是否有打开的文件描述符,因为他们怎么知道?
  • 打印实际的文件描述符(通过f.fileno())表明它们被重复使用,即文件已关闭。请注意,.close 是 Python 级别的函数,而 _io 是用 C 实现的,其类将直接调用其 C 函数,而不是 Python 包装器。
  • James:如果您仔细阅读文档,您会发现无法保证已删除的对象会被垃圾回收。
  • @JamesMchugh 您是否将__del__del 混淆了? Python 确实使用了 _io.TextIOWrapper.__del__ 的等价物,但它是一个 C 函数,调用 Python 级别的 f.close
  • @JamesMchugh del 仅取消链接名称。因此,这可能会将引用计数降低到 0(在 CPython 中)或最终触发垃圾收集(任何实现)。它不直接调用__del__

标签: python io


【解决方案1】:

self.closeself 上保留一个引用,因此您正在编写self.close = lambda: self.close(),创建一个循环引用。结果:

  1. del 什么都不做,CPython 不会回收对象,直到发生实际的 GC 收集,无论是在某个时候隐式地还是通过显式的 gc.collect()
  2. CPython 必须打破循环,所以当它开始收集对象时,它可能已经从中删除了属性

如果您将 strongold_close 的引用替换为弱引用,您可以非常清楚地看到这一点:

def wrap_close(old_close):
    old_close = weakref.ref(old_close)
    def new_close(*args, **kwargs):
        print("closing")
        c = old_close()
        if c:
            c(*args, **kwargs)
    return new_close

f = open('/dev/zero')
f.close = wrap_close(f.close)
f.close() # prints "closing"

f = open('/dev/zero')
f.close = wrap_close(f.close)
del f # nothing printed

打印"closing" 不止两次而是三次:

  • close() 被显式调用时
  • 当第一个 f 被垃圾时(创建第二个时)
  • 当第二个被删除时

我的问题是,调用 del 方法时不自动关闭文件或流有什么好处?

Python 在完成时绝对关闭文件对象。

【讨论】:

  • 循环引用不会阻止对象被收集。 CPython gc 明确存在仅用于循环引用。
  • @MisterMiyagi 长期不会阻止对象被收集,但del 不会这样做(您可以通过为文件创建弱引用来看到并在 del 之后检查弱引用的活跃度,在普通文件上弱引用是空的,而这里不是)。即使在强制 GC 运行之后,“替换方法”也不会被调用,可能是因为 CPython 决定通过首先取消设置可调用对象来打破循环。
  • 所以在这种情况下,close 的包装器可能会在实际文件流之前被删除。我没有考虑到这一点。最重要的是,仅仅因为del 可能会将对象的指针计数减少到 0,这并不意味着它会在此时被 GC。我想我现在有了更好的理解。谢谢。
  • @JamesMchugh CPython 或多或少保证 0 的引用计数将导致收集(请注意,这是特定于 cpython,它通常不是有效的)但是如果你有一个循环重新计数永远不会为 0,对象存在于自己的 refcount 无法到达的气泡中。此外,如果您使用weakrefs,您可以清楚地看到集合本身没有问题。
  • 所以使用弱引用消除了这个循环,因为它不能保护底层对象免受 GC,从而允许引用计数达到 0。这非常有趣。我什至不知道你可以在 Python 中使用弱引用。感谢您让我意识到这一点。