python生成器垃圾收集

时间:2018-04-29 21:37:31

标签: python generator python-internals

我认为我的问题与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  

是否可以保证在“完成计数”之前打印“在最后一个块中”这一行?或者这是因为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的表面价值看作,但在发电机的背景下思考让我三思而后行。

2 个答案:

答案 0 :(得分:23)

正如您所料,您依赖于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生成器 - 与上面的内容不同,这是生成器类型的公共接口的一部分:

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' answer指出的那样,您在第二次测试中依赖 依赖于CPython编译器的特定于实现的行为。

答案 1 :(得分:16)

我支持@ abarnert的答案,但因为我已经输入了这个......

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

在第二个示例中,生成器迭代器仍然绑定到c,直到main()退出,因此CPython知道您可能恢复{ {1}}随时都有。在c退出之前,它不是“垃圾”。一个更高级的编译器可以注意到在循环结束后永远不会引用main(),并且在此之前决定有效c,但del c不会尝试预测未来。所有本地名称保持绑定,直到您自己明确解除绑定,或者它们本地结束的范围。