阻止TextIOWrapper以Py2 / Py3兼容的方式关闭GC

时间:2015-06-23 04:21:46

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

我需要完成的任务:

给定二进制文件,以几种不同的方式对其进行解码,提供 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

我可以通过覆盖__del__在Python 3中解决这个问题(这对我的用例是一个合理的解决方案,因为我可以完全控制解码过程,我只需要在最后公开一个非常统一的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之间看起来非常相似所以我不明白为什么继承自__del__的{​​{1}}不能覆盖Python 2(甚至在IOBase中可见),但似乎仍然被执行。 Python 3完全按预期工作。

我能做些什么吗?

4 个答案:

答案 0 :(得分:3)

编辑:

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中使用。

答案 1 :(得分:3)

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

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

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

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

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

答案 2 :(得分:0)

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

答案 3 :(得分:0)

编辑:

我找到了一个更好的解决方案(相对而言),但是如果对任何人都有用的话,我会留下这个答案。 (这是展示gc.garbage

的一种非常简单的方法

请不要使用以下内容。

OLD:

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

我们可以做的是在析构函数中设置一个循环引用,它将阻止GC事件。然后,我们可以查看garbage gc来查找这些不可引用的对象,打破周期并删除该引用。

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可能是透明的。我仍然希望有办法覆盖__del__的{​​{1}}。