skg*_*nga 27 python generator python-internals
我认为我的问题与此有关,但并不完全相似.考虑以下代码:
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()
Run Code Online (Sandbox Code Playgroud)
此代码的输出是:
Counting... 10
Counting... 9
Counting... 8
Counting... 7
Counting... 6
In the finally block
Finished counting
Run Code Online (Sandbox Code Playgroud)
是否保证在"完成计数"之前打印"在最后一个块中"这一行?或者这是因为cPython实现细节,当引用计数达到0时,对象将被垃圾收集.
另外我对如何好奇finally的块countdown执行发电机?例如,如果我将代码更改main为
def main():
c = countdown(10)
for n in c:
if n == 5:
break
print('Counting... ', n)
print('Finished counting')
Run Code Online (Sandbox Code Playgroud)
然后我确实看到Finished counting之前打印过In the finally block.垃圾收集器如何直接进入finally块?我认为我总是把try/except/finally它看作是面子,但在发电机的背景下思考让我三思而后行.
aba*_*ert 24
正如您所料,您依赖于CPython引用计数的特定于实现的行为.1
事实上,如果你在PyPy中运行这段代码,输出通常是:
Counting... 10
Counting... 9
Counting... 8
Counting... 7
Counting... 6
Finished counting
In the finally block
Run Code Online (Sandbox Code Playgroud)
如果你在一个交互式PyPy会话中运行它,那么最后一行可能会在很多行之后出现,甚至只有当你最终退出时.
如果你看一下如何实现生成器,它们的方法大致如下:
def __del__(self):
self.close()
def close(self):
try:
self.raise(GeneratorExit)
except GeneratorExit:
pass
Run Code Online (Sandbox Code Playgroud)
当引用计数变为零时,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')
Run Code Online (Sandbox Code Playgroud)
(如果你离开了raise,你会得到相同的结果只是略有不同的原因.)
你可以明确地close生成一个 - 并且,与上面的东西不同,这是生成器类型的公共接口的一部分:
def main():
c = countdown(10)
for n in c:
if n == 5:
break
print('Counting... ', n)
c.close()
print('Finished counting')
Run Code Online (Sandbox Code Playgroud)
或者,当然,您可以使用with声明:
def main():
with contextlib.closing(countdown(10)) as c:
for n in c:
if n == 5:
break
print('Counting... ', n)
print('Finished counting')
Run Code Online (Sandbox Code Playgroud)
1.正如蒂姆·彼得斯的回答指出的那样,你们也在第二次测试中依赖于CPython编译器的特定于实现的行为.
Tim*_*ers 16
我支持@ abarnert的答案,但因为我已经输入了这个......
是的,第一个示例中的行为是CPython引用计数的工件.当您退出循环时,countdown(10)返回的匿名生成器 - 迭代器对象将丢失其最后一个引用,因此立即进行垃圾收集.这又会触发发电机finally:组.
在你的第二个例子中,生成器迭代器一直保持c到你的main()出口为止,所以CPython知道你可以c随时恢复.在main()退出之前它不是"垃圾" .一个更高级的编译器可能会注意到c在循环结束后从未引用过,并且del c在此之前决定有效,但CPython不会尝试预测未来.所有本地名称保持绑定,直到您自己明确解除绑定,或者它们本地结束的范围.