首页 > 技术文章 > part7-2 Python 的异常处理(异常传播轨迹、traceback模块使用、异常处理规则),异常处理练习

Micro0623 2019-11-07 10:47 原文


一、 Python 的异常传播轨迹

异常对象有提供一个 with_traceback 用于处理异常的传播轨迹,查看异常的传播轨迹可追踪异常触发的源头,也可看到异常一路触发的轨迹。示例如下:
 1 class SelfException(Exception): pass
 2 
 3 def main():
 4     firstMethod()
 5 def firstMethod():
 6     secondMethod()
 7 def secondMethod():
 8     thirdMethod()
 9 def thirdMethod():
10     raise SelfException("自定义异常信息")
11 main()
12 
13 输出信息如下所示:
14 Traceback (most recent call last):
15   File "traceback_test.py", line 15, in <module>
16     main()
17   File "traceback_test.py", line 8, in main
18     firstMethod()
19   File "traceback_test.py", line 10, in firstMethod
20     secondMethod()
21   File "traceback_test.py", line 12, in secondMethod
22     thirdMethod()
23   File "traceback_test.py", line 14, in thirdMethod
24     raise SelfException("自定义异常信息")
25 __main__.SelfException: 自定义异常信息
从上面输出可知,异常从 thirdMethod() 函数开始触发,传到 secondMethod() 函数,再传到 firstMethod() 函数,最后传到main() 函数,在 main() 函数终止,这个过程就是 Python 的异常传播轨迹。

实际开发中,复杂操作都会分解成一系列函数或方法调用。因为为了具有更好的可重用性,会将每个可重用的代码单元定义成函数或方法,将复杂任务逐渐分解为更易管理的小型子任务。通常一个大的业务功能需要由多个函数或方法来共同完成,在最终编程模型中,很多对象将通过一系列函数或方法调用来实现通信,执行任务。

程序运行时发生的一系列函数或方法调用,形成了“函数调用栈”。异常传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或者异常被处理后重新引发新异常),异常就从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者······直到最后传到 Python 解释器,此时 Python 解释器会中止该程序,并打印异常的传播轨迹信息。

在上面输出信息中,出现了很多的错误,但实际只有一个错误,系统提示的多行信息,是在显示异常依次触发的轨迹。上面的输出信息记录了程序中执行停止的各个点。最后一行显示了异常的类型和异常的详细消息。从这一行向上,逐个记录了异常发生源头、异常依次传播所经过的轨迹,并标明异常发生在哪个文件、哪一行、哪个函数处。

Python 提供的 traceback 模块专门用于处理异常传播轨迹,traceback 模块中提供了两个常用方法
(1)、traceback.print_exc(): 将异常传播轨迹信息输出到控制台或指定文件中。
(2)、format_exe(): 将异常传播轨迹信息转换成字符串。

在 print_exc() 方法中省略了两个参数 limit、file,加上参数的形式是:print_exc([limit[, file]]),然而这个形式的完整形式是 print_exception(etype, value, tb[, limit[, file]]),这个完整形式中,前面三个参数用于分别指定异常的如下信息:
(1)、etype: 指定异常类型。
(2)、value: 指定异常值。
(3)、tb: 指定异常的 traceback 信息。

当程序在 except 块中时,该 except 块所捕获的异常信息可通过 sys 对象来获取,其中 sys.exc_type、sys.exc_value、sys.exc_traceback 就代表当前 except 块内的异常类型、异常值和异常传播轨迹。此时 print_exc([limit[, file]]) 相当于如下形式:
print_exception(sys.exc_type, sys.exc_value, sys.exc_tb[, limit[, file]])

也就是使用 print_exc([limit[, file]]) 会自动处理当前 except 块所捕获的异常。该方法涉及的两个参数是:

(1)、limit: 用于限制显示异常传播的层数,比如函数 A 调用函数 B,函数 B 发生了异常,如果指定 limit=1,则只显示函数 A 里面发生的异常。不设置 limit 参数的话,默认全部显示。
(2)、file: 指定将异常传播轨迹信息输出到指定文件中。不指定该参数,则输出到控制台。

借助 traceback 模块,可以使用 except 块捕获异常,并在其中打印异常传播信息,包括把它输出到文件中。示例如下:
 1 import traceback        # 导入 traceback 模块
 2 class SelfException(Exception): pass
 3 
 4 def main():
 5     firstMethod()
 6 def firstMethod():
 7     secondMethod()
 8 def secondMethod():
 9     thirdMethod()
10 def thirdMethod():
11     raise SelfException("自定义异常信息")
12 try:
13     main()
14 except:
15     # 捕获异常,并将异常传播信息输出到控制台
16     traceback.print_exc()
17     # 捕获异常,将异常传播信息输出到指定文件中
18     traceback.print_exc(file=open('log.txt', 'a'))
上面代码中首先导入 traceback 模块,接下来使用 except 捕获程序的异常,并使用 traceback 的 print_exc() 方法输出异常传播信息,分别将它输出到控制台和指定指定文件中。运行上面的代码可以在控制台看到输出的异常传播信息,并且在程序文件所在的目录下生成一个 log.txt 文件,该文件同样记录了异常传播信息。

二、 异常处理规则

成功的异常处理应该实现如下4个目标:
(1)、使程序代码混乱最小化。
(2)、捕获并保留诊断信息。
(3)、通知合适的人员。
(4)、采用合适的方式结束异常活动。

1、 不要过度使用异常
滥用异常机制会带来负面影响,过度使用异常体现在两个方面:
(1)、把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单的引发异常来代替所有的错误处理。
(2)、使用异常处理来代替流程控制。

对于已知的错误和普通错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对于外部的、不能确定和预知的运行时错误才使用异常。例如在五子棋游戏中,处理用户输入坐标点已有棋子的两种方式:
# 如果下棋的点不为空(已有棋子)
if board[int(y_str) - 1][int(x_str) - 1] != "":
    inputStr = input("你输入的坐标点已有棋子,请重新输入\n")
    continue
这种处理方式简洁明了、逻辑清晰,程序运行效率也很好,程序进入 if 块后,即结束了本次循环。如果将上面的处理方式改为如下形式:
# 如果要下棋的点不为空(已有棋子)
if board[int(y_str) - 1][int(x_str) - 1] != "":
    # 引发默认的 RuntimeError 异常
    raise
这种处理方式没有提供有效的错误处理代码,检测到下棋的坐标点有棋子时,没有做相应的处理,只简单的引发一个异常。这种处理方式虽然简单,但 Python 解释器接收到这个异常后,还需要进入相应的 except 块来捕获该异常,所以运行效率要差一些,并且下棋重复这个错误完全可预料,所以程序完全可以针对该错误提供相应的处理,而不是引发异常。

要注意的是:异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。

2、 不要使用太庞大的 try 块
try 块的代码过于庞大会造成 try 块中出现异常的可能性大大增加,导致分析异常原因的难度也大大增加。try 块很庞大时,后面需要大量的 except 块才可针对不同的异常提供不同的处理逻辑,在同一个 try 块后使用大量的 except 块则需要分析它们之间的逻辑关系,反而增加编程复杂度。

所以大的 try 块可分割成多个可能出现异常的程序段落,并将其放在单独的 try 块中,从而分别捕获并处理异常。

3、 不要忽略捕获到的异常
当产生异常时,在 except 块里应做一些有用的事情,如处理并修复异常,不能将 except 块设置为空或者简单的打印异常信息。对异常采取适当的措施,例如:
(1)、处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续运行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作等等,总之,程序应尽量修复异常,使程序能恢复运行。
(2)、重新引发新异常。把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新上传给上层调用者。
(3)、在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用 except 语句来捕获该异常,让上层调用者来负责处理该异常。

三、异常小结

1、Python 异常处理主要依赖 try、except、else、finally 和 raise 五个关键字,以及这五个关键字用法。
2、异常类之间的继承关系。
3、异常捕获的详细处理方法,以及怎样使用 raise 根据业务需求引发自定义异常。
4、实际运用中常用的异常处理方式,以及异常处理的几条基本规则。

练习:

1、提示输入一个整数N,表示接下来可以输入N个数字字符串,每个字符串用空格分隔,程序将每个数字字符串用空格分割成两个整数,并计算这两个整数整除的结果。使用异常处理机制来处理用户输入的各种错误情况,并提示用户重新输入。
str_N = input("请输入整数N:")
try:
    n = int(str_N)
    print(n)
    i = 0
    try:
        while True:
            a, b = input("请输入2个整数(空格分隔):").split()
            print(int(a) // int(b))
            i += 1
            if i >= n: break
    except:
        print("必须输入2个空格隔开的整数!")
except:
    print("请输入整数N!")

2、提示输入一个整数,如果输入的整数是奇数,则输出“有趣”;如果输入的整数是偶数,且在2~5之间,则输出“没意思”;如果输入的整数是偶数,且在 6~20 之间,则输出“有趣”;如果输入的是整数是其他偶数,则打印“没意思”。要求使用异常处理机制来处理用户输入的各种错误情况。
while True:
    str_n = input("请输入一个整数:")
    if str_n == 'q':
        import sys
        sys.exit(0)
    try:
        n = int(str_n)
        if n % 2 != 0:
            print("有趣")
        elif 5 >= n >= 2:
            print("没意思")
        elif 20 >= n >= 6:
            print("有趣")
        else:
            print("没意思")
    except:
        print("请按照要求输入一个整数!")

3、提供一个字符串元组,程序要求元组中每一个元素的长度都在 5~20 之间;否则,程序引发异常。
class LengthError(Exception): pass
def foo(tp):
    for s in tp:
        if not isinstance(s, str):
            raise ValueError("所有元素必须是字符串")
        if not (20 >= len(s) >= 5):
            raise LengthError("字符串长度要求在5~20之间!")
    print(tp)

if __name__ == '__main__':
    str_tuple = ('python', 'linuxb', 'michael', 'this', 'java')
    foo(str_tuple)

4、提示输入 x1, y1, x2, y2, x3, y3 六个数值,分别代表三个点的坐标,程序判断这三个点是否在同一条直线上。要求使用异常处理机制处理用户输入的各种错误情况,如果三个点不在同一条直线上,则程序出现异常。
while True:
    st = input("请输入3个点的x、y值,以空格分隔:")
    if st == 'q':
        import sys
        sys.exit(0)
    try:
        x1s, y1s, x2s, y2s, x3s, y3s = st.split()
        x1, y1, x2, y2, x3, y3 = float(x1s), float(y1s), float(x2s), float(y2s), float(x3s), float(y3s)
        if x1 == 0 and x2 ==0 and x3 == 0:
            print("在同一条直线上!")
        elif 0 in (x1, x2, x3):
            print("不在同一条直线上")
        elif y1 / x1 == y2 / x2 and y1 / x1 == y3 / x3:
            print("在同一条直线上!")
        else:
            print("不在同一条直线上!")
    except:
        print("必须输入6个空格隔开的整数!")

 

推荐阅读