我认为我的问题与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
的表面价值看作,但在发电机的背景下思考让我三思而后行。
答案 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
不会尝试预测未来。所有本地名称保持绑定,直到您自己明确解除绑定,或者它们本地结束的范围。