我想我可能会误解finallytry/ except/finally 块的子句在 Python 中对于生成器的工作原理。
在下面的代码块中,生成器启动一个线程,如果调用者因任何原因退出,则该线程将被清理。至少这是意图。
但是,我注意到在一些奇怪的情况下,该finally块不会运行:如果调用者引发然后捕获自己的异常,并将异常对象分配给变量,则finally不会运行。我不知道为什么会这样。
这是代码。
import signal
from threading import Thread
import time
class MyThread(Thread):
def __init__(self):
super().__init__()
self._stopped = False
def run(self):
while not self._stopped:
time.sleep(0.2)
def stop(self):
self._stopped = True
class ThreadRunner:
def start(self):
self._my_thread = MyThread()
self._my_thread.start()
def end(self):
self._my_thread.stop()
self._my_thread.join()
print('ThreadRunner end!')
def loop_forever():
thread_runner = ThreadRunner()
try:
thread_runner.start()
yield
finally:
print('loop_forever() is all done!') # When does this line get run?
thread_runner.end()
def listener():
print('listener begin!')
looper = loop_forever()
next(looper)
try:
raise Exception()
except Exception as e:
es = e # If you comment out this line (and replace it with `pass`), it doesn't hang
print('listener... done!')
def main():
def handle_exit(signum, *args):
raise Exception("SIGINT: EXITING")
signal.signal(signal.SIGINT, handle_exit)
listener()
if __name__ == "__main__":
main()
print('This program has exited. Or has it?')
Run Code Online (Sandbox Code Playgroud)
如果运行此代码,您将看到以下内容:
按 CTRL+C 触发信号处理程序。这将引发异常,从而触发finally相关问题。
正如我上面所说,如果删除es = e(将其替换为 pass),出于某种原因,我们的代码会按预期退出。
def listener():
print('listener begin!')
looper = loop_forever()
next(looper)
try:
raise Exception()
except Exception as e:
pass
# es = e # If you comment out this line (and replace it with `pass`), it doesn't hang
print('listener... done!')
Run Code Online (Sandbox Code Playgroud)
另外,如果我重写listener()为使用 for 循环而不是 next(),我们的代码将按预期退出:
def listener():
print('listener_using_next')
for _ in loop_forever():
try:
raise Exception()
except Exception as e:
es = e
print('listener_using_next... done!')
Run Code Online (Sandbox Code Playgroud)
编辑:为了回应一条评论,我想指出,即使您提前返回,中断生成器,我们的代码也会按预期退出。
def listener():
print('listener_using_next')
for _ in loop_forever():
try:
raise Exception()
except Exception as e:
es = e
return
print('listener_using_next... done!')
Run Code Online (Sandbox Code Playgroud)
最后,如果我在main()(使用 import gc;gc.collect())之后调用垃圾收集,我们的代码几乎会按预期退出:打印“此程序已退出”行,然后是“loop_forever() 已完成!”
if __name__ == "__main__":
main()
print('This program has exited. Or has it?')
import gc;gc.collect()
Run Code Online (Sandbox Code Playgroud)
有人可以向我解释一下finally:发电机的工作原理吗?理想情况下,以这样的方式有助于解释这种奇怪的行为?
finally生成器中的运行条件与任何其他代码相同:当try块的执行完成以及任何except被触发的块的执行时,运行finally。然而,相对于大多数非生成器代码,在生成器中更容易避免这些情况发生。
当您在 a 中间暂停发电机时try,发电机运行的条件finally尚未发生。如果发电机不再恢复,则finally运行的条件将永远不会出现。
为了尝试解决这个问题,当生成器被垃圾收集时,Python 会GeneratorExit向生成器抛出一个异常。这通常会导致生成器运行任何挂起的finally块并完成,但如果生成器捕获异常并再次挂起,或者如果生成器从未进行垃圾收集,则finally块可能不会运行。
在您的测试用例中,通过将异常保存到变量es,您可以创建一个引用循环(通过异常对象的堆栈跟踪),从而使生成器保持活动状态。es = e创建引用循环是必要的,因为 Python 在末尾附加了一个隐式,del e专门except用来避免引用循环。然后,Python 直到程序结束才运行垃圾收集,此时您的生成器最终被清理干净。
请注意,不能保证垃圾收集会运行,或者它会清理它“应该”清理的所有内容,并且正如您所看到的,即使它确实运行,也不必很快运行。
当您使用for循环而不是 时next,您不会让生成器悬浮在 的中间try。for运行生成器完成,立即自然地运行finally.
| 归档时间: |
|
| 查看次数: |
236 次 |
| 最近记录: |