【问题标题】:Prevent TextIOWrapper from closing on GC in a Py2/Py3 compatible way防止 TextIOWrapper 以兼容 Py2/Py3 的方式关闭 GC
【发布时间】:2015-06-23 04:21:45
【问题描述】:

我需要完成的事情:

给定一个二进制文件,通过提供 TextIOBase API 的几种不同方式对其进行解码。理想情况下,这些后续文件可以传递,而无需明确跟踪它们的生命周期。

不幸的是,包装 BufferedReader 会 当TextIOWrapper 超出范围时,导致该阅读器被关闭。

这是一个简单的演示:

In [1]: import io

In [2]: def mangle(x):
   ...:     io.TextIOWrapper(x) # Will get GCed causing __del__ to call close
   ...:     

In [3]: f = io.open('example', mode='rb')

In [4]: f.closed
Out[4]: False

In [5]: mangle(f)

In [6]: f.closed
Out[6]: True

我可以在 Python 3 中通过覆盖 __del__ 来解决这个问题(这对于我的用例来说是一个合理的解决方案,因为我可以完全控制解码过程,我只需要在最后公开一个非常统一的 API):

In [1]: import io

In [2]: class MyTextIOWrapper(io.TextIOWrapper):
   ...:     def __del__(self):
   ...:         print("I've been GC'ed")
   ...:         

In [3]: def mangle2(x):
   ...:     MyTextIOWrapper(x)
   ...:     

In [4]: f2 = io.open('example', mode='rb')

In [5]: f2.closed
Out[5]: False

In [6]: mangle2(f2)
I've been GC'ed

In [7]: f2.closed
Out[7]: False

但是这在 Python 2 中不起作用:

In [7]: class MyTextIOWrapper(io.TextIOWrapper):
   ...:     def __del__(self):
   ...:         print("I've been GC'ed")
   ...:         

In [8]: def mangle2(x):
   ...:     MyTextIOWrapper(x)
   ...:     

In [9]: f2 = io.open('example', mode='rb')

In [10]: f2.closed
Out[10]: False

In [11]: mangle2(f2)
I've been GC'ed

In [12]: f2.closed
Out[12]: True

我花了一些时间盯着 Python 源代码,它在 2.7 和 3.4 之间看起来非常相似,所以我不明白为什么从 IOBase 继承的 __del__ 在 Python 2(或甚至在dir 中可见),但似乎仍然被执行。 Python 3 完全按预期工作。

有什么我可以做的吗?

【问题讨论】:

    标签: python python-2.7 python-3.x garbage-collection


    【解决方案1】:

    只需分离您的 TextIOWrapper() 对象,然后再将其作为垃圾回收:

    def mangle(x):
        wrapper = io.TextIOWrapper(x)
        wrapper.detach()
    

    TextIOWrapper() 对象仅关闭它所附加的流。如果您无法更改对象超出范围的代码,则只需本地保留对TextIOWrapper()对象的引用并在该点分离。

    如果您必须子类化TextIOWrapper(),那么只需在__del__ 挂钩中调用detach()

    class DetachingTextIOWrapper(io.TextIOWrapper):
        def __del__(self):
            self.detach()
    

    【讨论】:

      【解决方案2】:

      编辑:

      Just call detach first, thanks martijn-pieters!


      事实证明,对于 Python 2.7 中调用 close 的解构函数,基本上没有什么可以做的。这被硬编码到 C 代码中。相反,我们可以修改close,使其在__del__ 发生时不会关闭缓冲区(__del__ 将在 C 代码中的_PyIOBase_finalize 之前执行,让我们有机会改变close 的行为) .这让close 可以按预期工作,而无需让 GC 关闭缓冲区。

      class SaneTextIOWrapper(io.TextIOWrapper):
          def __init__(self, *args, **kwargs):
              self._should_close_buffer = True
              super(SaneTextIOWrapper, self).__init__(*args, **kwargs)
      
          def __del__(self):
              # Accept the inevitability of the buffer being closed by the destructor
              # because of this line in Python 2.7:
              # https://github.com/python/cpython/blob/2.7/Modules/_io/iobase.c#L221
              self._should_close_buffer = False
              self.close()  # Actually close for Python 3 because it is an override.
                            # We can't call super because Python 2 doesn't actually
                            # have a `__del__` method for IOBase (hence this
                            # workaround). Close is idempotent so it won't matter
                            # that Python 2 will end up calling this twice
      
          def close(self):
              # We can't stop Python 2.7 from calling close in the deconstructor
              # so instead we can prevent the buffer from being closed with a flag.
      
              # Based on:
              # https://github.com/python/cpython/blob/2.7/Lib/_pyio.py#L1586
              # https://github.com/python/cpython/blob/3.4/Lib/_pyio.py#L1615
              if self.buffer is not None and not self.closed:
                  try:
                      self.flush()
                  finally:
                      if self._should_close_buffer:
                          self.buffer.close()
      

      我之前的解决方案在这里使用了_pyio.TextIOWrapper,它比上面的要慢,因为它是用 Python 而不是 C 编写的。

      它涉及简单地用 noop 覆盖 __del__,这也适用于 Py2/3。

      【讨论】:

      • 这实在是太复杂了。 TextIOWrapper() 关闭包装缓冲区的唯一原因是因为它附加到它。你只需要.detach()对象,防止它关闭底层对象。
      • 天哪,这太明显了,我现在已经很好地摆脱了这个问题,但感谢这些年来提供正确的解决方案:D
      【解决方案3】:

      一个简单的解决方案是从函数返回变量并将其存储在脚本范围内,这样在脚本结束或对它的引用发生更改之前,它不会被垃圾收集。但可能还有其他优雅的解决方案。

      【讨论】:

        【解决方案4】:

        编辑:

        我找到了一个更好的解决方案(相对而言),但如果它对任何人都有用,我会留下这个答案。 (这是炫耀gc.garbage的一种非常简单的方式)

        请不要实际使用以下内容。

        旧:

        我找到了一个潜在的解决方案,虽然它很可怕:

        我们可以做的是在析构函数中设置一个循环引用,这将推迟 GC 事件。然后我们可以查看gcgarbage 以找到这些不可引用的对象,打破循环,并删除该引用。

        In [1]: import io
        
        In [2]: class MyTextIOWrapper(io.TextIOWrapper):
           ...:     def __del__(self):
           ...:         if not hasattr(self, '_cycle'):
           ...:             print "holding off GC"
           ...:             self._cycle = self
           ...:         else:
           ...:             print "getting GCed!"
           ...:
        
        In [3]: def mangle(x):
           ...:     MyTextIOWrapper(x)
           ...:     
        
        In [4]: f = io.open('example', mode='rb')
        
        In [5]: mangle(f)
        holding off GC
        
        In [6]: f.closed
        Out[6]: False
        
        In [7]: import gc
        
        In [8]: gc.garbage
        Out[8]: []
        
        In [9]: gc.collect()
        Out[9]: 34
        
        In [10]: gc.garbage
        Out[10]: [<_io.TextIOWrapper name='example' encoding='UTF-8'>]
        
        In [11]: gc.garbage[0]._cycle=False
        
        In [12]: del gc.garbage[0]
        getting GCed!
        
        In [13]: f.closed
        Out[13]: True
        

        说实话,这是一个非常可怕的解决方法,但它可能对我提供的 API 是透明的。我仍然更喜欢一种覆盖IOBase__del__ 的方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-10-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多