【问题标题】:Does 'finally' always execute in Python?“终于”总是在 Python 中执行吗?
【发布时间】:2018-08-22 01:49:01
【问题描述】:

对于 Python 中任何可能的 try-finally 块,是否保证始终执行 finally 块?

例如,假设我在 except 块中返回:

try:
    1/0
except ZeroDivisionError:
    return
finally:
    print("Does this code run?")

或者也许我重新提出Exception

try:
    1/0
except ZeroDivisionError:
    raise
finally:
    print("What about this code?")

测试表明,finally 确实在上述示例中被执行,但我想还有其他我没有想到的场景。

是否存在finally 块在 Python 中无法执行的情况?

【问题讨论】:

  • 我能想象到的唯一情况 finally 无法执行或“失败”是在无限循环、sys.exit 或强制中断期间。 documentation 声明 finally 总是被执行,所以我会这样做。
  • 如果电源线从墙上扯下来,finally 将不会执行。
  • 您可能对这个关于 C# 的同一问题的答案感兴趣:stackoverflow.com/a/10260233/88656
  • 在一个空的信号量上阻止它。永远不要发出信号。完成。
  • @Xay sys.exit 除了抛出异常什么都不做。用词不当。

标签: python exception-handling try-catch-finally finally


【解决方案1】:

“保证”比finally 的任何实现都强得多。可以保证的是,如果执行流出整个try-finally 构造,它将通过finally 执行此操作。不能保证的是执行会流出try-finally

  • A finally in a generator or async coroutine might never run,如果对象从不执行结束。有很多可能发生的方式;这是一个:

    def gen(text):
        try:
            for line in text:
                try:
                    yield int(line)
                except:
                    # Ignore blank lines - but catch too much!
                    pass
        finally:
            print('Doing important cleanup')
    
    text = ['1', '', '2', '', '3']
    
    if any(n > 1 for n in gen(text)):
        print('Found a number')
    
    print('Oops, no cleanup.')
    

    请注意,这个例子有点棘手:当生成器被垃圾回收时,Python 尝试通过抛出 GeneratorExit 异常来运行 finally 块,但这里我们捕获了该异常,然后再次捕获 yield,此时 Python 会打印一个警告(“生成器忽略 GeneratorExit”)并放弃。详情请见PEP 342 (Coroutines via Enhanced Generators)

    生成器或协程可能无法执行到结论的其他方式包括:对象从未被 GC 处理(是的,这是可能的,即使在 CPython 中也是如此),或者 __aexit__ 中的 async with awaits,或者如果对象 awaits 或 yields 在 finally 块中。此列表并非详尽无遗。

  • A finally in a daemon thread might never execute 如果所有非守护线程都先退出。

  • os._exit will halt the process immediately 不执行 finally 块。

  • os.fork 可能会导致 finally 阻塞到 execute twice。除了您预期两次发生的事情会出现正常问题外,如果对共享资源的访问不是correctly synchronized,这可能会导致并发访问冲突(崩溃、停顿等)。

    由于multiprocessing 在使用fork start method(Unix 上的默认值)时使用fork-without-exec 创建工作进程,然后在工作人员的工作完成后在工作人员中调用os._exitfinallymultiprocessing 交互可能会出现问题 (example)。

  • C 级分段错误将阻止 finally 块运行。
  • kill -SIGKILL 将阻止 finally 块运行。 SIGTERMSIGHUP 也将阻止 finally 块运行,除非您自己安装处理程序来控制关闭;默认情况下,Python 不处理 SIGTERMSIGHUP
  • finally 中的异常可能会阻止清理完成。一个特别值得注意的情况是,如果用户在我们开始执行finally 块时按了 control-C just。 Python 将引发KeyboardInterrupt 并跳过finally 块内容的每一行。 (KeyboardInterrupt-safe 代码很难写)。
  • 如果计算机断电,或者它进入休眠状态并且没有唤醒,finally 块将不会运行。

finally 块不是交易系统;它不提供原子性保证或任何类似的保证。其中一些示例可能看起来很明显,但很容易忘记此类事情可能会发生并过度依赖finally

【讨论】:

  • 我相信只有您列表中的第一点是真正相关的,并且有一个简单的方法可以避免它:1)永远不要使用裸露的except,并且永远不要在生成器中捕获GeneratorExit .关于线程/杀死进程/segfaulting/关机的要点是预期的,python 不能做魔术。另外:finally 中的异常显然是个问题,但这并不会改变控制流 移至finally 块的事实。关于Ctrl+C,您可以添加一个忽略它的信号处理程序,或者在当前操作完成后简单地“安排”一次干净的关机。
  • 提到kill -9在技术上是正确的,但有点不公平。任何语言编写的程序都不会在收到 kill -9 时运行任何代码。事实上,根本没有任何程序接收 kill -9,所以即使它想接收,它也无法执行任何操作。这就是 kill -9 的全部意义所在。
  • @Tom:关于kill -9 的观点没有指定语言。坦率地说,它需要重复,因为它处于盲点。太多的人忘记了,或者没有意识到,他们的程序可能会在不被允许清理的情况下停止运行。
  • @GiacomoAlzetta:有些人依赖finally 块,就好像他们提供了交易保证一样。似乎很明显,他们没有,但这并不是每个人都意识到的。至于生成器的情况,有很多方法可能根本不会对生成器进行 GC,并且生成器或协程在GeneratorExit 之后可能会意外产生很多方法,即使它没有捕获GeneratorExit ,例如,如果 async with 挂起 __exit__ 中的协程。
  • @user2357112 是的 - 几十年来,我一直在努力让开发人员在应用程序启动时清理临时文件等,而不是退出。靠着所谓的‘收拾收拾’,是在求失望和泪水:)
【解决方案2】:

是的。 最后总是赢。

击败它的唯一方法是在finally: 有机会执行之前停止执行(例如,使解释器崩溃、关闭计算机、永远暂停生成器)。

我想还有其他我没有想到的场景。

还有一些你可能没有想到的:

def foo():
    # finally always wins
    try:
        return 1
    finally:
        return 2

def bar():
    # even if he has to eat an unhandled exception, finally wins
    try:
        raise Exception('boom')
    finally:
        return 'no boom'

根据您退出解释器的方式,有时您最终可以“取消”,但不是这样:

>>> import sys
>>> try:
...     sys.exit()
... finally:
...     print('finally wins!')
... 
finally wins!
$

使用不稳定的os._exit(我认为这属于“崩溃解释器”):

>>> import os
>>> try:
...     os._exit(1)
... finally:
...     print('finally!')
... 
$

我目前正在运行这段代码,以测试 finally 在宇宙热死后是否仍会执行:

try:
    while True:
       sleep(1)
finally:
    print('done')

不过,我仍在等待结果,请稍后再回来查看。

【讨论】:

  • 或在 try catch 中有一个 i 有限循环
  • finally in a generator or coroutine can quite easily fail to execute,不会出现“解释器崩溃”的情况。
  • 在宇宙的热寂时间不复存在之后,sleep(1) 肯定会导致未定义的行为。 :-D
  • @StevenVascellaro 我认为这没有必要——os._exit 实际上与引发崩溃(不干净的退出)相同。正确的退出方式是sys.exit
  • @wim 你有关于 While 循环的更新吗? :P 谢谢你的回答,这些例子对我很有帮助
【解决方案3】:

根据Python documentation

无论之前发生了什么,一旦代码块完成并处理任何引发的异常,就会执行最终块。即使在异常处理程序或 else 块中出现错误并引发了新的异常,最终块中的代码仍会运行。

还需要注意的是,如果有多个return语句,包括finally块中的一个,那么finally块return是唯一会执行的。

【讨论】:

    【解决方案4】:

    嗯,是的,也不是。

    可以保证的是,Python 将始终尝试执行 finally 块。如果您从块中返回或引发未捕获的异常,finally 块会在实际返回或引发异常之前执行。

    (您可以通过简单地运行问题中的代码来控制自己)

    我能想象的唯一不会执行 finally 块的情况是 Python 解释器本身崩溃,例如在 C 代码中或由于断电。

    【讨论】:

    • 哈哈.. 或者try catch中存在无限循环
    • 我认为“嗯,是与否”是最正确的。 Finally: always wins 其中“always”表示解释器能够运行并且“finally:”的代码仍然可用,“wins”定义为解释器将尝试运行 finally : 阻塞,它会成功。那是“是”,这是非常有条件的。 “否”是解释器在“最终”之前可能停止的所有方式:- 电源故障,硬件故障,针对解释器的 kill -9,解释器或它所依赖的代码中的错误,其他挂起解释器的方式。以及挂在“finally:”里面的方法。
    【解决方案5】:

    我发现这个没有使用生成器函数:

    import multiprocessing
    import time
    
    def fun(arg):
      try:
        print("tried " + str(arg))
        time.sleep(arg)
      finally:
        print("finally cleaned up " + str(arg))
      return foo
    
    list = [1, 2, 3]
    multiprocessing.Pool().map(fun, list)
    

    睡眠可以是任何可能运行时间不一致的代码。

    这里似乎发生的是,第一个完成的并行进程成功地离开了 try 块,但随后尝试从函数返回一个尚未在任何地方定义的值 (foo),这会导致异常。该异常会终止映射,而不允许其他进程到达它们的 finally 块。

    此外,如果您在 try 块中的 sleep() 调用之后添加行 bar = bazz。然后到达该行的第一个进程抛出异常(因为未定义 bazz),这会导致其自己的 finally 块运行,但随后会终止映射,导致其他 try 块在没有到达它们的 finally 块的情况下消失,并且第一个进程也没有到达它的返回语句。

    这对于 Python 多处理意味着您不能信任异常处理机制来清理所有进程中的资源,即使其中一个进程也可能出现异常。额外的信号处理或管理多处理映射调用之外的资源是必要的。

    【讨论】:

      猜你喜欢
      • 2011-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-01
      • 2020-10-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多