关于异常处理:’finally’总是在Python中执行吗?

Does 'finally' always execute in Python?

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

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

1
2
3
4
5
6
try:
    1/0
except ZeroDivisionError:
    return
finally:
    print("Does this code run?")

或者我再养一只Exception

1
2
3
4
5
6
try:
    1/0
except ZeroDivisionError:
    raise
finally:
    print("What about this code?")

测试表明,finally确实针对上述示例执行,但我认为还有其他一些场景我没有想到。

在任何情况下,finally块都可能无法在python中执行吗?


"保证"这个词比任何finally的实现都要有力得多。可以保证的是,如果执行从整个tryfinally构造中流出,它将通过finally来执行。不能保证的是执行将从tryfinally流出。

  • 如果对象从未执行到结论,那么生成器或异步协程中的finally可能永远不会运行。有很多种可能发生的方法;这里有一种:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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会打印一个警告("generator忽略generatorexit")并放弃。有关详细信息,请参阅PEP 342(通过增强的生成器进行协同工作)。

    生成器或协同程序可能无法执行以得出结论的其他方式包括:如果对象只是从未被提取过(是的,这是可能的,即使在cpython中),或者如果对象async withawait__aexit__中,或者如果对象awaits或yieldfinally块中。这份清单并非详尽无遗。

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

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

  • os.fork可能导致finally块执行两次。如果对共享资源的访问没有正确同步,这可能会导致并发访问冲突(崩溃、暂停等),这与您预期发生两次的正常问题一样。

    由于multiprocessing在使用fork start方法(Unix上的默认方法)时使用不带exec的fork来创建工作进程,然后在工作进程中调用os._exit,一旦工作进程完成,finallymultiprocessing交互可能会有问题(例如)。

  • C级分段故障将阻止finally块运行。
  • kill -SIGKILL将阻止finally块运行。SIGTERMSIGHUP也会阻止finally块运行,除非您自己安装一个处理程序来控制关闭;默认情况下,python不处理SIGTERMSIGHUP。小精灵
  • finally中的异常可以阻止清理完成。一个特别值得注意的情况是,当我们开始执行finally块时,用户点击control-c。python将提升一个KeyboardInterrupt并跳过finally块内容的每一行。(KeyboardInterrupt安全代码很难编写)。
  • 如果计算机断电,或者休眠而不唤醒,finally块将无法运行。

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


对。最终总是胜利。

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

I imagine there are other scenarios I haven't thought of.

以下是一些你可能没有想到的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
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'

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

1
2
3
4
5
6
7
8
>>> import sys
>>> try:
...     sys.exit()
... finally:
...     print('finally wins!')
...
finally wins!
$

使用不稳定的os._exit(我认为这属于"翻译崩溃"):

1
2
3
4
5
6
7
>>> import os
>>> try:
...     os._exit(1)
... finally:
...     print('finally!')
...
$

我目前正在运行此代码,以测试宇宙热死后是否最终仍将执行:

1
2
3
4
5
try:
    while True:
       sleep(1)
finally:
    print('done')

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


根据python文档:

No matter what happened previously, the final-block is executed once the code block is complete and any raised exceptions handled. Even if there's an error in an exception handler or the else-block and a new exception is raised, the code in the final-block is still run.

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


嗯,是和否。

可以保证的是,python将始终尝试执行finally块。在从块返回或引发未捕获异常的情况下,最终块将在实际返回或引发异常之前执行。

(只需运行问题中的代码就可以控制自己的行为)

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
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),这会导致异常。该异常会在不允许其他进程到达其最终块的情况下终止映射。

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

对于Python多处理来说,这意味着,如果任何一个进程都有异常,就不能信任异常处理机制来清理所有进程中的资源。需要额外的信号处理或管理多处理映射调用之外的资源。


要真正了解它的工作原理,只需运行以下两个示例:

  • 1
    2
    3
    4
    5
    6
    try:
        1
    except:
        print 'except'
    finally:
        print 'finally'

    将输出

    finally

  • 1
    2
    3
    4
    5
    6
    try:
        1/0
    except:
        print 'except'
    finally:
        print 'finally'

    将输出

    except
    finally