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 counting
在 In 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
没有尝试来预测未来。所有本地名称都保持绑定状态,直到您自己明确解除绑定,或者它们的本地范围结束。
我认为我的问题与 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 counting
在 In 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.正如
我赞同@abarnert 的回答,但因为我已经输入了这个...
是的,您第一个示例中的行为是 CPython
引用计数的产物。当您跳出循环时,返回的匿名生成器-迭代器对象 countdown(10)
将丢失其最后一个引用,因此会立即被垃圾回收。这反过来会触发生成器的 finally:
套件。
在你的第二个例子中,生成迭代器一直绑定到 c
直到你的 main()
退出,据 CPython
知道你 可能 随时恢复 c
。直到 main()
退出才 "garbage"。更高级的编译器 可以 注意到循环结束后从未引用 c
,并决定在此之前有效地 del c
,但 CPython
没有尝试来预测未来。所有本地名称都保持绑定状态,直到您自己明确解除绑定,或者它们的本地范围结束。