如果不再使用Python生成器但尚未达到StopIteration,它会被垃圾回收吗?



当生成器不再使用时,应该对其进行垃圾收集,对吗?我试了下面的代码,但我不确定哪一部分我错了。

import weakref
import gc
def countdown(n):
    while n:
        yield n
        n-=1
cd = countdown(10)
cdw = weakref.ref(cd)()
print cd.next()
gc.collect()
print cd.next()
gc.collect()
print cdw.next()

在最后第二行,我调用了垃圾收集器,因为没有再调用cd了。gc应该释放cd。但是当我调用cdw.next()时,它仍然打印8。我尝试了更多的cdw.next(),它可以成功地打印所有其余的,直到StopIteration。

我这样做是因为我想了解生成器和协程是如何工作的。在David Beazley的PyCon演讲"关于协程和并发的好奇课程"的第28张幻灯片上,他说协程可能会无限期地运行,我们应该使用.close()来关闭它。然后他说垃圾收集器会调用.close()。在我的理解中,一旦我们自己调用.close(), gc就会再次调用.close()gc会收到一个警告,它不能在一个已经关闭的协程上调用.close()吗?

由于python的动态特性,直到您到达当前例程的末尾才释放对cd的引用,因为(至少)python的Cpython实现不会"提前读取"。(如果你不知道你正在使用什么python实现,几乎可以肯定是"Cpython")。有许多微妙之处使得解释器几乎不可能确定一个对象是否应该是自由的,如果它在一般情况下仍然存在于当前命名空间中(例如,你仍然可以通过调用locals()到达它)。

在一些不太常见的情况下,其他python实现可能能够在当前堆栈帧结束之前释放对象,但Cpython不会这样做。

试试下面这段代码,它展示了生成器在Cpython中是可以自由清理的:

import weakref
def countdown(n):
    while n:
        yield n
        n-=1
def func():
    a = countdown(10)
    b = weakref.ref(a)
    print next(a)
    print next(a)
    return b
c = func()
print c()

对象(包括生成器)在其引用计数达到0时被垃圾收集(在Cpython中——其他实现可能工作方式不同)。在Cpython中,只有当您看到del语句时,或者当对象因当前命名空间更改而超出作用域时,引用计数才会减少。

重要的是,一旦不再有对对象的引用,它就可以被垃圾收集器清除。实现如何确定没有更多引用的细节留给您正在使用的特定python发行版的实现者。

在您的示例中,直到脚本结束时才会收集生成器的垃圾。Python不知道你是否会再次使用cd,所以它不能丢弃它。准确地说,在全局命名空间中仍然有一个指向生成器的引用。当生成器的引用计数降为零时,生成器将获得GCed,就像任何其他对象一样。即使发电机没有耗尽。

这可能发生在很多正常情况下-如果它在一个超出作用域的本地名称中,如果它是del ed,如果它的所有者获得GCed。但是,如果任何活动对象(包括名称空间)持有对它的强引用,它将不会被GCed。

Python垃圾收集器并没有那么聪明。即使在这行之后不再引用cd,引用仍然存在于局部变量中,因此它不能被收集。(事实上,您正在使用的某些代码可能会在局部变量中挖掘并重新启用它。不太可能,但有可能。所以Python不能做任何假设。)

如果你想让垃圾收集器在这里做点什么,试着添加:

del cd

这将删除局部变量,允许对象被收集。

其他答案解释了gc.collect()不会对仍然引用它的任何东西进行垃圾收集。仍然有一个对生成器的活动引用cd,因此在删除cd之前它不会被gc化。

但是,OP还使用这一行创建了对该对象的第二个强引用,它调用弱引用对象:

cdw = weakref.ref(cd)()
因此,如果执行del cd并调用gc.collect(),生成器仍然不会被gc,因为cdw也是一个引用。

要获得一个实际的弱引用,不要调用weakref.ref对象。只需这样做:

cdw = weakref.ref(cd)

现在,当删除cd并收集垃圾时,引用计数将为零,并且调用弱引用将导致None,正如预期的那样。

最新更新