防止 TextIOWrapper 以 Py2/Py3 兼容的方式关闭 GC
Prevent TextIOWrapper from closing on GC in a Py2/Py3 compatible way
我需要完成的任务:
给定一个二进制文件,以几种不同的方式对其进行解码,提供 TextIOBase
API。理想情况下,这些后续文件可以传递,而无需我明确跟踪它们的生命周期。
不幸的是,包装 BufferedReader
将
导致 reader 在 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 之间看起来非常相似,所以我不明白为什么 __del__
继承自 IOBase
在 Python 2 中不可覆盖(甚至在 dir
中可见),但似乎仍会执行。 Python 3 完全符合预期。
有什么我可以做的吗?
一个简单的解决方案是 return 函数中的变量并将其存储在脚本范围内,这样在脚本结束或对它的引用发生更改之前,它不会被垃圾回收。但可能还有其他优雅的解决方案。
编辑:
我找到了一个更好的解决方案(相对而言),但如果对任何人学习有用,我会留下这个答案。 (这是一种非常简单的炫耀方式gc.garbage
)
请不要实际使用以下内容。
旧:
我找到了一个潜在的解决方案,尽管它很糟糕:
我们可以做的是在析构函数中设置一个循环引用,这将推迟GC事件。然后我们可以查看 gc
的 garbage
来找到这些不可引用的对象,打破循环,并删除该引用。
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__
。
编辑:
事实证明,在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()
对象,然后再对其进行垃圾回收:
def mangle(x):
wrapper = io.TextIOWrapper(x)
wrapper.detach()
TextIOWrapper()
对象只关闭它附加到的流。如果您无法更改对象超出范围的代码,则只需在 本地 保留对 TextIOWrapper()
对象的引用并在该点分离。
如果您必须子类TextIOWrapper()
,那么只需在__del__
钩子中调用detach()
:
class DetachingTextIOWrapper(io.TextIOWrapper):
def __del__(self):
self.detach()
我需要完成的任务:
给定一个二进制文件,以几种不同的方式对其进行解码,提供 TextIOBase
API。理想情况下,这些后续文件可以传递,而无需我明确跟踪它们的生命周期。
不幸的是,包装 BufferedReader
将
导致 reader 在 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 之间看起来非常相似,所以我不明白为什么 __del__
继承自 IOBase
在 Python 2 中不可覆盖(甚至在 dir
中可见),但似乎仍会执行。 Python 3 完全符合预期。
有什么我可以做的吗?
一个简单的解决方案是 return 函数中的变量并将其存储在脚本范围内,这样在脚本结束或对它的引用发生更改之前,它不会被垃圾回收。但可能还有其他优雅的解决方案。
编辑:
我找到了一个更好的解决方案(相对而言),但如果对任何人学习有用,我会留下这个答案。 (这是一种非常简单的炫耀方式gc.garbage
)
请不要实际使用以下内容。
旧:
我找到了一个潜在的解决方案,尽管它很糟糕:
我们可以做的是在析构函数中设置一个循环引用,这将推迟GC事件。然后我们可以查看 gc
的 garbage
来找到这些不可引用的对象,打破循环,并删除该引用。
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__
。
编辑:
事实证明,在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()
对象,然后再对其进行垃圾回收:
def mangle(x):
wrapper = io.TextIOWrapper(x)
wrapper.detach()
TextIOWrapper()
对象只关闭它附加到的流。如果您无法更改对象超出范围的代码,则只需在 本地 保留对 TextIOWrapper()
对象的引用并在该点分离。
如果您必须子类TextIOWrapper()
,那么只需在__del__
钩子中调用detach()
:
class DetachingTextIOWrapper(io.TextIOWrapper):
def __del__(self):
self.detach()