首页 > 解决方案 > python生成器垃圾收集

问题描述

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

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  

是否保证将在“完成计数”之前打印“在 finally 块中”行?或者这是因为 cPython 的实现细节,当引用计数达到 0 时,对象将被垃圾回收。

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

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

然后我确实看到Finished counting之前打印过In the finally block。垃圾收集器如何直接去finally块?我想我一直都接受try/except/finally它的表面价值,但在发电机的背景下思考让我三思而后行。

标签: pythongeneratorpython-internals

解决方案


正如您所料,您依赖于 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 会立即删除对象(它还有一个垃圾收集器来分解循环引用,但这在这里不相关)。一旦生成器超出范围,它就会被删除,因此它会被关闭,因此它将 a 提升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生成一个生成器——而且,与上面的内容不同,这是生成器类型的公共接口的一部分:

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. 正如Tim Peters 的回答所指出的,您在第二个测试中依赖于 CPython 编译器的特定于实现的行为。


推荐阅读