Python 生成器中的“finally”何时运行?

Ada*_*ing 5 python

我想我可能会误解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

按 CTRL+C 触发信号处理程序。这将引发异常,从而触发finally相关问题。

异常导致Python最终触发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:发电机的工作原理吗?理想情况下,以这样的方式有助于解释这种奇怪的行为?

use*_*ica 5

finally生成器中的运行条件与任何其他代码相同:当try块的执行完成以及任何except被触发的块的执行时,运行finally。然而,相对于大多数非生成器代码,在生成器中更容易避免这些情况发生

当您在 a 中间暂停发电机时try,发电机运行的条件finally尚未发生。如果发电机不再恢复,则finally运行的条件将永远不会出现。

为了尝试解决这个问题,当生成器被垃圾收集时,Python 会GeneratorExit向生成器抛出一个异常。这通常会导致生成器运行任何挂起的finally块并完成,但如果生成器捕获异常并再次挂起,或者如果生成器从未进行垃圾收集,则finally块可能不会运行。


在您的测试用例中,通过将异常保存到变量es,您可以创建一个引用循环(通过异常对象的堆栈跟踪),从而使生成器保持活动状态。es = e创建引用循环是必要的,因为 Python 在末尾附加了一个隐式,del e专门except用来避免引用循环。然后,Python 直到程序结束才运行垃圾收集,此时您的生成器最终被清理干净。

请注意,不能保证垃圾收集会运行,或者它会清理它“应该”清理的所有内容,并且正如您所看到的,即使它确实运行,也不必很快运行。


当您使用for循环而不是 时next,您不会让生成器悬浮在 的中间tryfor运行生成器完成,立即自然地运行finally.