当某些单一模块作用域中的变量被引用时,在什么时候该引用会被解释器释放?
在尝试在一个工具模块中实现一个清理例程时,为了解决我的问题,我最终决定使用一个weakref回调来进行清理。但是,由于该模块内部存在对该对象的强引用,因此我担心它无法按预期工作。
为了说明这一点,我们来看一个例子:
# foo_lib.py
class Foo(object):
_refs = {}
def __init__(self, x):
self.x = x
self._weak_self = weakref.ref(self, Foo._clean)
Foo._refs[self._weak_self] = x
@classmethod
def _clean(cls, ref):
print 'cleaned %s' % cls._refs[ref]
foo = Foo()
其他类随后引用了foo_lib.foo。我确实找到了一个来自1.5.1的旧文档,它引用了我的担忧,但没有任何文档让我完全相信foo将以一种方式释放,调用回调能够可靠地触发。有人可以为我提供一些文档,以便为我解决这个问题吗?
解决方案
回答1:
Python模块在退出时被清理,并且任何__del__方法都有可能被调用:
- 不保证对仍在解释器退出时存在的对象的
__del__()方法进行调用。 - 以下划线开头的名称首先被清除:从1.5版本开始,Python保证名称以单个下划线开头的全局变量在其模块中的其他全局变量被删除之前被删除;如果不存在其他对这些全局变量的引用,这可能有助于确保导入的模块在调用
__del__()方法时仍然可用。 - 弱引用回调依赖于与
__del__方法相同的机制:C释放函数(类型->tp_dealloc)。 foo实例将保留对Foo._clean类方法的引用,但全局名称Foo可能已被清除(它在CPython中分配为None);一旦注册了回调,你的方法应该是安全的,因为它从不引用Foo。
回答2:
这里要做正确的事情是明确释放你的强引用,而不是依赖关闭来执行此操作。
特别是,如果模块被释放,它的全局变量将被释放,但任何地方似乎都没有记录模块将被释放。因此,在关闭时仍然可能引用你的对象。并且,正如Martijn Pieters指出的那样:
- 不保证对仍在解释器退出时存在的对象的
__del__()方法进行调用。
但是,如果你可以确保在解释器退出之前某个时间没有对你的对象的(非弱)引用,那么你可以保证你的清理运行。你可以在关闭之前使用atexit处理程序或在主模块的末尾显式地执行此操作(或调用sys.exit,或完成你的最后一个非守护线程,或任何其他操作)。要做的最简单的事情通常是将你的整个主函数包装在一个with或try/finally中。或者,更简单地说,不要将清理代码放入__del__方法或弱引用回调中;只需将清理代码本身放入你的with或finally或atexit中。
在另一个答案的评论中:
-
我实际上要尝试关闭一个通常由计时器保持打开的子进程,但需要在程序退出时将其关闭。这样做是否唯一可靠的方式是启动一个守护进程来监视和单独杀死另一个进程?
-
通常处理此类事情的方法是将计时器替换为某个外部可发出信号的东西。在不知道你的应用程序体系结构和你所使用的计时器类型(例如,一个单线程异步服务器,其中反应器踢计时器,或一个单线程异步GUI应用程序,其中OS计时器消息踢计时器,或一个多线程应用程序,其中计时器只是一个在间隔之间休眠的线程,或…)的情况下,很难更具体地解释。同时,你可能还想查看是否有更简单的方法来处理你的子进程。例如,也许使用一个显式的进程组,并杀死你的进程组而不是你的进程(这将杀死所有子进程,无论是在Windows还是Unix…尽管细节非常不同)?或者给子进程命名管道,当管道另一端关闭时让它退出?
请注意,文档也不会给你任何关于剩余引用被删除的顺序(如果有的话)的保证。事实上,如果你使用的是CPython,Py_Finalize明确表示它是“随机完成的”。
源代码很有趣。显然它没有明确随机化,甚至不是完全任意的。它首先进行GC收集直到没有剩下任何东西,然后它最终化GC本身,然后它执行PyImport_Cleanup(基本上只是sys.modules.clear()),然后有一个注释出来的收集(有一些关于为什么如此的讨论),最后是一个_PyImport_Fini(仅定义为“仅限内部使用”)。
但这意味着,假设你的模块确实持有你对象的唯一(非弱)引用,并且没有涉及模块本身的不可中断的循环,那么你的模块将在关闭时被清理,这将释放对你的对象的最后一个引用,导致它也一起被清理掉。(当然,你不能指望内置函数、扩展模块和你直接引用的东西以外的任何东西仍然存在…但上面的代码应该没问题,因为foo不能在Foo之前被清理,并且它不依赖于任何其他非内置函数。)
请记住,这是CPython特有的——事实上,是CPython 3.3特有的;你将需要阅读你版本的相关等效资源以确保。再次强调,文档明确指出事情“以随机顺序”删除,所以如果你不想依赖实现特定行为,那么这就是你必须期待的。
当然,你的清理代码仍然无法保证被调用。例如,未处理的信号(在Unix中)或结构化异常(在Windows中)将杀死解释器而不会给它清理任何东西的机会。即使你为此编写了一些处理程序,也有人总是可以拔掉电源线。因此,如果你需要一个完全稳健的设计,你需要在任何时候都能够在不进行清理的情况下进行中断(通过日记记录、使用原子文件操作、具有显式确认的协议等)。