“最终”是否总是在Python中执行?


126

对于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中执行?


18
我能想象的唯一情况是finally无法执行或“ sys.exit破坏其目的”是在无限循环或强制中断期间。该文档各国finally始终执行,所以我与去。
Xay

1
有点横向思考,不一定要问您什么,但是我很确定,如果您打开“任务管理器”并终止该进程,finally将不会运行。如果计算机在:D
Alejandro

144
finally如果从墙上撕下电源线,将不会执行。
user253751 '18

3
您可能对以下有关C#的相同问题的答案感兴趣:stackoverflow.com/a/10260233/88656
Eric Lippert,

1
将其阻止在空的信号灯上。永远不要发信号。做完了
马丁·詹姆斯

Answers:


206

“保证”一词比任何finally应得的实现都要强大得多。什么是保证的是,如果执行全部的流出try- finally结构,它会通过finally这样做。无法保证执行将流出try- finally

  • finally中一台发电机或异步协同程序可能永远不会运行,如果对象根本不会执行到结束。可能有很多方式发生。这是一个:

    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尝试finally通过抛出GeneratorExit异常来运行该块,但是这里我们捕获了该异常,然后yield再次出现,此时Python打印警告(“生成器忽略了GeneratorExit ”)并放弃。有关详细信息,请参见PEP 342(通过增强型生成器的协程)

    发电机或协同程序可能不会执行到结束的其他方式包括:如果对象只是从来没有GC'ed(是的,这是可能的,即使在CPython的),或者如果async with awaitS IN __aexit__,或者如果对象awaitS或yieldS IN一个finally块。此列表并非详尽无遗。

  • finally如果所有非守护程序线程都首先退出,则守护程序线程中的A 可能永远不会执行

  • os._exit将立即停止该进程而不执行finally块。

  • os.fork可能导致finally执行两次。如果您对共享资源的访问未正确同步,则可能会导致并发访问冲突(崩溃,停顿等),这不仅会发生两次常见的正常问题,还会导致并发访问冲突。

    由于multiprocessing在使用fork start方法(Unix上的默认设置)时使用fork-without-exec创建工作进程,然后os._exit在工作程序完成后调用工作程序,finally因此multiprocessing交互可能会出现问题(示例)。

  • C级分段故障将阻止finally块运行。
  • kill -SIGKILL将阻止finally块运行。SIGTERM并且SIGHUP也将阻止finally运行,除非你安装一个处理器来控制自己的关断块; 默认情况下,Python不处理SIGTERMSIGHUP
  • 中的异常finally会阻止清理完成。其中特别值得注意的情况是,如果用户点击控制-C 只是因为我们已经开始执行该finally块。Python将引发KeyboardInterrupt并跳过该finally块内容的每一行。(KeyboardInterrupt-safe代码很难编写)。
  • 如果计算机断电,或者休眠且无法唤醒,finally则无法运行数据块。

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


14
我相信列表的第一点才是真正相关的,有一种简单的方法可以避免它:1)永远不要使用裸机except,也不要陷入GeneratorExit发电机内部。关于线程/杀死进程/ segfaulting /关闭电源的观点是可以预期的,python不能做魔术。另外:异常输入finally显然是一个问题,但这不会改变控制流移动到finally块的事实。关于Ctrl+C,您可以添加一个忽略它的信号处理程序,或者在当前操作完成后简单地“计划”干净关闭。
Giacomo Alzetta

8
提到kill -9在技​​术上是正确的,但是有点不公平。收到kill -9时,任何用任何语言编写的程序都不会运行任何代码。实际上,任何程序都不会收到 kill -9,因此即使愿意,它也无法执行任何操作。这就是杀死-9的全部要点。
汤姆(Tom)

10
@Tom:重点kill -9没有指定语言。坦率地说,它需要重复,因为它位于盲点。太多的人忘记或不意识到,他们的程序甚至可能被禁止清理而无法停止。
cHao

5
@GiacomoAlzetta:那里有人依赖finally区块,好像他们提供了交易担保一样。他们似乎没有这样做,但这并不是每个人都意识到的。至于生成器的情况,有很多方法可能根本不对生成器进行GC运算,并且生成器或协程GeneratorExit即使无法捕获到GeneratorExit,也可能会意外屈服,例如,如果async with挂起里的一个协程__exit__
user2357112支持莫妮卡

2
@ user2357112是的-我数十年来一直在努力让开发人员在应用启动时清理临时文件等,而不是退出。依靠所谓的“清理和优雅终止”,要求失望和眼泪:)
Martin James

69

是。 最后总是胜利。

克服它的唯一方法是在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!')
... 
$

我当前正在运行以下代码,以测试在宇宙热死之后,是否最终仍然可以执行:

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

但是,我仍在等待结果,因此请稍后再检查。


5
或尝试捕获中有一个有限循环–在
间谍活动

8
finally生成器或协程中的代码很容易无法执行,而不会在“使解释器崩溃”的情况下发生。
user2357112支持Monica

27
在宇宙热死之后,时间不再存在,因此sleep(1)肯定会导致不确定的行为。:-D
David Foerster,

您可能想在“唯一破坏它的方法是使编译器崩溃”之后直接提及_os.exit。现在,在最终获胜的例子之间混杂着。
Stevoisiak

2
@StevenVascellaro我认为这不是必需的- os._exit就所有实际目的而言,它与引发崩溃(退出出口不干净)相同。退出的正确方法是sys.exit
wim

9

根据Python文档

无论以前发生了什么,一旦代码块完成并处理了所有引发的异常,便会执行final块。即使异常处理程序或else块中存在错误,并且引发了新的异常,final块中的代码仍将运行。

还应注意,如果有多个return语句,包括finally块中的一个语句,则finally块返回是唯一将执行的语句。


8

好吧,是的,不是。

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

(只要运行问题中的代码,您本可以控制自己的一切)

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


哈哈..或尝试抓捕中存在无限循环
Sapy

我认为“是的,是的,不是”是最正确的。最终:在“始终”意味着解释器能够运行并且“最终:”的代码仍然可用的地方,总是赢,并且“胜利”被定义为解释器将尝试运行finally:块,它将成功。那是“是”,而且是有条件的。“否”是解释器在“最终”之前可能停止的所有方式:-电源故障,硬件故障,针对解释器的kill -9,解释器或它依赖的代码错误,其他悬挂解释器的方式。而挂在里面的方法“最后:”。
比尔四世

1

我没有使用生成器功能就发现了这一点:

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块。

另外,如果您bar = bazz在try块中的sleep()调用之后添加该行。然后,到达该行的第一个进程将引发异常(因为未定义bazz),这将导致运行其自己的finally块,但随后杀死该映射,从而导致其他try块消失而未到达其finally块,并且第一个过程也不到达其return语句。

这对于Python多处理意味着什么,即使哪一个进程都可能有异常,您也不能相信异常处理机制来清理所有进程中的资源。在多处理映射调用之外需要其他信号处理或管理资源。


-2

接受的答案的附录,只是为了帮助了解它的工作原理,并提供了一些示例:

  • 这个:

     try:
         1
     except:
         print 'except'
     finally:
         print 'finally'

    将输出

    最后

  •    try:
           1/0
       except:
           print 'except'
       finally:
           print 'finally'

    将输出

    除了
    最后


这根本无法回答问题。它只是当它的例子工程
子轩
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.