结论

先简单总结下try语句块,Python中的异常处理使用try...except...[else]...[finally]的方式,其中的方括号表示是可选的。所以一个最全的try语句块如下所示:

try:
    statement1
except A:
    // A异常处理
    statement2
except:
    // 通用异常处理
    statement3
else:
   some statement block
finally:
   some statement block

:为了说明的更清楚,约定一下文中的“try语句块”指的是try、except、else、finally所有的部分;而“try代码块”只指try里面正常执行的代码块,即只代表上面的statement1。

其执行逻辑为:

  1. 先执行try代码块,

    • 如果出现异常,则开始匹配后面的except列表,如果匹配到具体的异常,则进程异常处理,否则进入到通用的except异常处理(即except后面什么也没有,这种可以捕获任何类型的异常)里面。需要注意的是:通用异常语句一定要写在具体的异常语句之后,不然会有语法错误
    • 如果没有出现任何异常,则执行else里面的语句。
  2. 最后执行finally里面的语句。

当然,这些都是最基本的,不是本文的重点,本文要分析的是一些容易产生混淆的场景:当finally和return等语句结合的时候执行顺序是什么样?

我们先上结论,然后再逐一验证:

以下内容来自Python官方文档the-try-statement部分:

If finally is present, it specifies a ‘cleanup’ handler. The try clause is executed, including any except and else clauses. If an exception occurs in any of the clauses and is not handled, the exception is temporarily saved. The finally clause is executed. If there is a saved exception it is re-raised at the end of the finally clause. If the finally clause raises another exception, the saved exception is set as the context of the new exception. If the finally clause executes a return or break statement, the saved exception is discarded.

以下内容来自Python官方文档Defining Clean-up Actions部分:

A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. When an exception has occurred in the try clause and has not been handled by an except clause (or it has occurred in an except or else clause), it is re-raised after the finally clause has been executed. The finally clause is also executed “on the way out” when any other clause of the try statement is left via a break, continue or return statement.

两处说的其实是一个意思,我们从中整理出一些结论:

  1. finally代码块永远是try语句块中最后一个执行的
  2. 如果在finally代码块发生了异常,并且在执行到finally代码块之前该异常还没有被处理,那么该异常会被暂存;然后:

    • 如果finally里面没有发生新的异常,并且没有returnbreak语句,那么在finally执行结束后,会重新抛出之前暂存的异常;
    • 如果finally里面没有发生新的异常,但执行了returnbreak语句,那原来暂存的异常就会被丢弃,代码正常跳出try语句块;
    • 如果在finally里面发生了新的异常,那原来暂存的异常会作为新异常的上下文一起被抛出。

注:暂存的异常被存储在sys模块里面,可通过sys.exc_info()访问,该函数返回一个3个元素的元组:(异常类,异常实例,traceback对象)。

下面我们用一些例子来验证一下上面的结论。

结论1验证

关于第1条结论,主要是要理解最后两个字,最后指的是程序运行即将离开try语句块的时候,主要有returnbreakcontinue三种情况。

return的情况:

def demo1():
    try:
        raise RuntimeError("To Force Issue")
        return 1
    except:
        return 2
    else:
        return 3
    finally:
        return 4

if __name__ == "__main__":
    print(demo1())

输出结果如下:

4

demo1的的运行顺序为:先执行try,发生异常,跳到except里面,执行return 1,最后再执行finally里面的return 4。所以demo1函数最终返回的是4而非2。

break的情况:

def demo2():
    cnt = 0
    while True:
        try:
           cnt += 1
           if cnt == 5:
               break 
        finally:
            print(cnt)

if __name__ == "__main__":
    demo2()

输出为:

1
2
3
4
5

可以看到,每一轮循环都会跳出try语句块,但跳出之前都执行一遍finally。另外看最后一次,当cnt等于5的时候,执行了break,即将跳出while循环,同时也要跳出try语句块的时候(即上面所谓的"on the way out"),跳出之前还是执行了finally,打印了5。

continue的情况:

def demo3():
    cnt = 0
    while cnt < 5:
        try:
            cnt += 1
            continue 
        finally:
            print(cnt)

if __name__ == "__main__":
    demo3()

运行结果:

1
2
3
4
5

break一样,每一轮循环都会跳出try语句块,但都执行了finally。

所以,只要try语句块中有finally,那么它一定是最后执行的。因为这样,一般不建议在finally里面写return语句,这样会使其他地方的return语句都失效。finally一般只做一些资源释放的操作。

结论2验证

结论2首先是否和结论1的,即finally肯定是最后被执行的,只是如果产生了异常,并且在执行到finally的时候该异常还没有被处理(可能是没有匹配到except,也可能是处理异常的时候又产生了新异常),那就有3种情况了:

先看情况1:如果finally里面没有发生新的异常,并且没有returnbreak语句,那么在finally执行结束后,会重新抛出之前暂存的异常;

def demo4():
    try:
        raise RuntimeError("To Force Issue")
        return 1
    except:
        print('when handle exception, a new excetion occured')
        5 / 0
    else:
        return 3
    finally:
        print('in finally')

if __name__ == "__main__":
    demo4()

运行结果:

when handle exception, a new excetion occured
in finally
Traceback (most recent call last):
  File "/Users/allan/Desktop/temp/test.py", line 3, in demo4
    raise RuntimeError("To Force Issue")
RuntimeError: To Force Issue

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/allan/Desktop/temp/test.py", line 14, in <module>
    print(demo4())
  File "/Users/allan/Desktop/temp/test.py", line 7, in demo4
    5 / 0
ZeroDivisionError: division by zero

可以看到,先是执行try代码块,产生了RuntimeError异常,在except中处理该异常时,有产生了新的ZeroDivisionError异常,导致在执行到finally代码块时,异常还没有处理掉,所以之前的两个异常被先暂存起来,等finally里面的print语句执行结束之后,finally将刚才暂存的异常重新抛了出来。

再看情况2:如果finally里面没有发生新的异常,但执行了returnbreak语句,那原来暂存的异常就会被丢弃,代码正常跳出try语句块;

我们把demo4做简单修改:在finally里面加一个return(加break效果一样):

def demo5():
    try:
        raise RuntimeError("To Force Issue")
        return 1
    except:
        print('when handle exception, a new excetion occured')
        5 / 0
    else:
        return 3
    finally:
        print('in finally')
        return

if __name__ == "__main__":
    demo5()

运行结果:

when handle exception, a new excetion occured
in finally

可见之前demo4里面的异常信息没有了,因为finally里面有return,导致暂存的异常被丢弃了。所以再次不建议在finally里面写return还有break。另外Python语法不支持finally里面有continue。

最后看情况3:如果在finally里面发生了新的异常,那原来暂存的异常会作为新异常的上下文一起被抛出。

我们把demo5做小修改,在finally中增加一条会产生异常的语句:

def demo6():
    try:
        raise RuntimeError("To Force Issue")
        return 1
    except:
        print('when handle exception, a new excetion occured')
        5 / 0
    else:
        return 3
    finally:
        print('in finally')
        2 + 's'

if __name__ == "__main__":
    demo6()

执行结果:

when handle exception, a new excetion occured
in finally
Traceback (most recent call last):
  File "/Users/allan/Desktop/temp/test.py", line 3, in demo5
    raise RuntimeError("To Force Issue")
RuntimeError: To Force Issue

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/allan/Desktop/temp/test.py", line 7, in demo5
    5 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/allan/Desktop/temp/test.py", line 15, in <module>
    demo5()
  File "/Users/allan/Desktop/temp/test.py", line 12, in demo5
    2 + 's'
TypeError: unsupported operand type(s) for +: 'int' and 'str'

可以看到异常栈有三层,分别是try代码块中的RuntimeError、except代码块中的ZeroDivisionError、finally里面的TypeError,前二者在进入finally之前被暂存,因为finally里面发生了新异常,暂存的异常就作为新异常的一部分被一起抛出。