python 生成器垃圾回收

python generators garbage collection

我认为我的问题与 this 有关,但不完全相似。考虑这段代码:

def countdown(n):
    try:
        while n > 0:
            yield n
            n -= 1
    finally:
        print('In the finally block')

def main():
    for n in countdown(10):
        if n == 5:
            break
        print('Counting... ', n)
    print('Finished counting')

main()

这段代码的输出是:

Counting...  10      
Counting...  9       
Counting...  8       
Counting...  7       
Counting...  6       
In the finally block 
Finished counting  

是否保证 "In the finally block" 行会在 "Finished counting" 之前打印?或者这是因为 cPython 实现细节,当引用计数达到 0 时,对象将被垃圾回收。

另外我很好奇 countdown 生成器的 finally 块是如何执行的?例如如果我将 main 的代码更改为

def main():
    c = countdown(10)
    for n in c:
        if n == 5:
            break
        print('Counting... ', n)
    print('Finished counting')

然后我确实看到 Finished countingIn the finally block 之前打印出来了。垃圾收集器如何直接到finally块呢?我想我一直认为 try/except/finally 它的面值,但在生成器的背景下思考让我三思而后行。

如您所料,您依赖于 CPython 引用计数的特定实现行为。1

事实上,如果你 运行 这段代码在 PyPy 中,输出通常是:

Counting...  10
Counting...  9
Counting...  8
Counting...  7
Counting...  6
Finished counting
In the finally block

如果你在交互式 PyPy 会话中 运行 它,最后一行可能会在很多行之后出现,甚至只有在你最终退出时才出现。


如果您查看生成器的实现方式,它们的方法大致如下:

def __del__(self):
    self.close()
def close(self):
    try:
        self.raise(GeneratorExit)
    except GeneratorExit:
        pass

CPython 会在引用计数变为零时立即删除对象(它还有一个垃圾收集器来分解循环引用,但这与此处无关)。一旦生成器超出范围,它就会被删除,因此它会被关闭,因此它会在生成器框架中引发一个 GeneratorExit 并恢复它。当然 GeneratorExit 没有处理程序,所以 finally 子句被执行并且控制传递到堆栈,异常被吞没。

在使用混合垃圾收集器的 PyPy 中,直到 GC 决定下次扫描时才会删除生成器。在内存压力较低的交互式会话中,这可能会延迟到退出时间。但一旦这样做,同样的事情就会发生。

您可以通过显式处理 GeneratorExit 来查看:

def countdown(n):
    try:
        while n > 0:
            yield n
            n -= 1
    except GeneratorExit:
        print('Exit!')
        raise
    finally:
        print('In the finally block')

(如果您关闭 raise,您会得到相同的结果,只是原因略有不同。)


您可以显式 close 一个生成器——而且,与上面的东西不同,这是生成器类型的 public 接口的一部分:

def main():
    c = countdown(10)
    for n in c:
        if n == 5:
            break
        print('Counting... ', n)
    c.close()
    print('Finished counting')

或者,当然,您可以使用 with 语句:

def main():
    with contextlib.closing(countdown(10)) as c:
        for n in c:
            if n == 5:
                break
            print('Counting... ', n)
    print('Finished counting')

1.正如 指出的那样,您 在第二个测试中依赖于 CPython 编译器的特定于实现的行为。

我赞同@abarnert 的回答,但因为我已经输入了这个...

是的,您第一个示例中的行为是 CPython 引用计数的产物。当您跳出循环时,返回的匿名生成器-迭代器对象 countdown(10) 将丢失其最后一个引用,因此会立即被垃圾回收。这反过来会触发生成器的 finally: 套件。

在你的第二个例子中,生成迭代器一直绑定到 c 直到你的 main() 退出,据 CPython 知道你 可能 随时恢复 c。直到 main() 退出才 "garbage"。更高级的编译器 可以 注意到循环结束后从未引用 c,并决定在此之前有效地 del c,但 CPython 没有尝试来预测未来。所有本地名称都保持绑定状态,直到您自己明确解除绑定,或者它们的本地范围结束。