异常处理已经成为判断一门编程语言是否成熟的标准,除传统的像C语言没有提供异常机制之外,目前主流的编程语言如Python、Java、Kotlin等都提供了成熟的异常机制。异常机制可以使程序中的异常处理代码和正常业务代码分离,保证代码更加优雅,并可以提高程序的健壮性。
Python的异常机制主要依赖try、except、else、finally和raise五个关键字,其中在try关键字后缩进的代码简称try块,它里面放置的是可能引发异常的代码:在except后对应的是异常类型和一个代码块,用于表明该except块处理这种类型的代码块;在多个except之后可以放一个else块,表明程序不出现异常时还要执行else块;最后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行;而raise用于引发一个实际的异常,raise可以单独作为语句使用,引发一个具体的异常对象。
7.1异常概述
异常机制已经成为衡量一门编程语言是否成熟的标准之一,使用异常处理机制的Python程序有更好的容错性,更加健壮。
对于计算机程序而言,情况就更复杂了——没有人能保证自己写的程序永远没有出错!就算程序没有错误,你能保证用户总是按照你的意愿来输入?就算用户都是非常“聪明而且配合”的,你能保证运行该程序的操作系统永远稳定?你能保证运行该程序的硬件不会突然坏掉?你能保证网络永远通顺……你无法保证的情况太多了。
对于一个程序设计人员来说,需要尽可能预知所有可能发生的情况,尽可能保证程序在所有糟糕的情形下都可以运行。
高傲的程序员在开发程序时更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事。”
如果每次在实现真正的业务逻辑之前,都需要不厌其烦地考虑各种可能出错的情况,针对各种错误情况给出补救措施——这是多么乏味的事情啊。程序员喜欢解决问题,喜欢开发带来的“创造”快感,但不喜欢像一个“堵漏”的工人,去堵那些由外在条件造成的“漏洞”。
提示:对于构造大型、健壮,可维护的应用而言,错误处理是整个应用需要考虑的重要方面,程序员不能仅仅只做“对”的事情——程序员开发程序的过程,是一个创造的过程,这个过程需要全面的考虑,仅做“对”的事情是远远不够的。
对于上面的错误处理机制,主要有如下两个缺点:
- 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
- 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。
7.2异常处理机制
Python的异常处理机制可以让程序具有极好的容错性,让程序更加健壮,当程序运行出现意外情况时,系统会自动生成一个Error对象来通知程序,从而实现将“业务实现代码”和“错误处理代码”分离,提供更好的可读性。
7.2.1 使用try…except捕获异常
正如前面代码所提示的,希望有一个非常强大的“if块”,可以表示所有的错误情况,让程序一次处理所有的错误,也就是希望将错误集中处理。
出于这种考虑,此处试图把“错误代理代码”从“业务实现代码”中分离出来。
Python提出一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try块中定义,把所有的异常处理逻辑放在except块中进行处理。
try:#业务实现代码...
except (Error1,Error2,...)as e:alert 输入不合法goto retry
如果在执行try块的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交个Python解释器,这个过程被称为引发异常。
当Python解释器受到异常对象时,会寻找能处理该异常对象的except块,如果找到合适的except块,则把该异常对象交给该except块处理,这个过程被称为捕获异常。如果Python解释器找不到捕获异常的except块,则运行时环境终止,Python解释器也会退出。
提示:不管程序代码块是否处于try块中,甚至把包括except块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个Error对象。如果程序没有为这段代码定义任何的except块,则Python解释器无法找到处理该异常的except块,程序就在此退出。
下面使用异常处理机制来改写五子棋游戏中用户下棋部分的代码:
inputStr=input("请输入您下棋的坐标,应以x,y的格式:\n")
while inputStr!=None:try:#将用户输入的字符串以逗号(,)作为分隔符,分割成两个字符串x_str,y_str=inputStr.split(sep=",")#如果要下棋的点不为空if board[int(y_str)-1][int(x_str)-1]!="+":inputStr=input("您输入的坐标点已有棋子了,请重新输入\n")continue#把对应的列表元素赋为“·”board[int(y_str)-1][int(x_str)-1]="·"except Exception:inputStr=input("您输入的坐标不合法,请重新输入,下棋坐标应以x,y的格式\n")continue...
上面程序把处理用户输入字符串的代码都放在try块里执行,只要用户输入的字符串不是有效的坐标值(包括字母不能正确解析,没有逗号不能正确解析,解析出来的坐标引起数组越界…),系统就将引发一个异常对象,并把这个异常对象交给对应的except块(也就是上面except下的代码)处理。except块的处理方式是向用户提示坐标不合法,然后使用continue忽略本次循环剩下的代码,开始执行下一次循环。这就保证了该五子棋游戏有足够的容错性——用户可以随意输入,程序不会因为用户输入不合法而突然退出,程序不会疑问用户输入不合法而退出,程序会向用户提示输入不合法,让用户再次输入。
7.2.2 异常类的继承体系
当Python解释器接收到异常对象时,如何为该异常对象寻找except块呢?注意上面程序中的except块的except Exception:,这意味着每个except块都是专门用于处理该异常类及其子类的异常实例。
当Python解释器接收到异常对象后,会依次判断异常对象是否是except块后的异常类或其子类的实例,如果是,Python解释器将调用该except块来处理该异常;否则,再次拿该异常对象和下一个except块里的异常类进行比较。
从上图中可以看出,在try块后可以有多个except块,这时为了针对不同的异常类提供不同的异常处理方式。当程序发生不同的以外情况时,系统会发生不同的异常情况,Python解释器就会根据该异常对象所属的异常类来决定使用哪个except块来处理该异常。
通过在try块后提供多个except块可以无须在处理块中使用if判断异常类型,但依然可以针对不同的异常类型提供相应的处理逻辑,从而提供更细致、更有条理的异常处理逻辑。
图中可以看出,通常情况下,如果try块被执行一次,则try块后只有一个except块会被执行,不可能有多个except块被执行。除非在循环中使用continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个except块被执行。
Python所有异常类都从BaseException派生而来,提供了丰富的异常类,这些异常类之间有严格的继承关系。
Python的所有异常类的基类是BaseException,但如果用户要实现自定义异常,则不应该继承这个基类,而是应该继承Exception类。
BaseException的主要子类就是Exception,不管是系统的异常类,还是用户自定义的异常类都应该是从Exception派生。
import systry:a=int(sys.argv[1])b=int(sys.argv[2])c=a/bprint("您输入的两个数相除的结果是:",c)except IndexError:print("索引错误:运行程序时输入的参数个数不够")except ValueError:print("数值错误:程序只能收取帧数参数")except ArithmeticError:print("算术错误")except Exception:print("未知异常")
上面程序导入了sys模块,并通过sys模块的argv列表来获取运行Python程序时提供的参数。其中sys.argv[0]通常代表正在运行的Python程序名,sys.argv[1]代表运行程序所提供的第一个参数,sys.agrv[2]代表运行程序所提供的第二个参数……依次类推。
提示:Python使用import语句来导入模块,关于模块和导入模块在后面会讲解。
上面程序针对IndexError、ValueError、ArithmeticError类型的异常,提供了专门的异常处理逻辑。该程序运行时的异常处理逻辑可能有如下几种情形:
- 如果在运行该程序时输入的参数不够,将会发生索引错误,Python将调用IndexError对应的except块处理该异常。
- 如果在运行该程序时输入的参数不是数字,而是字母,将发生数值错误,Python将调用ValueError对应的except块处理该异常。
- 如果在运行该程序时输入的第二个参数是0,将发生除0异常,Python将调用ArithmeticError对应的except块处理该异常。
- 如果在程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Python将调用Exception对应的except块处理该异常。
上面程序中的三种异常,都是非常常见的运行异常,应该记住这些异常,并掌握在哪些情况下可能出现这些异常。
正如前面程序中所看到的,程序总是把对应Exception类的except块放在后面,这是为什么,在上面的异常李永成捕获流程图中,可以知道:如果把Exception类的对应的except块排在其他的except块的前面,Python解释器将直接进入该except块(因为所有的异常对象都是Exception或其子类的实例),而排在它后面的except将永远不会获得执行的机会。
实际上,在进行异常捕获时不仅应该把Exception类对应的except块放在最后,而且所有父类异常的except代码块都应该排在子类异常的except块的后面(即:先处理小异常,再处理大异常)。
提示:虽然Python语法没有要求,但在实际编程中一定要记住先捕获小一场,再捕获大异常。
7.2.3 多异常捕获
Python的一个except块可以捕获多种类型的异常。
在使用一个except块捕获多种类型的异常时,只要讲多个异常类用圆括号括起来,中间用逗号隔开即可——其实就是构建多个异常类的元组。
import sys
try:a=int(sys.argv[1])b=int(sys.argv[2])c=a/bprint("您输入的两个数相除的结果是:",c)
except (IndexError,ValueError,AttributeError):print("程序发生了数组越界、数字格式异常、算术异常之一")
except :print("未知异常")
7.2.4 访问异常信息
如果程序需要在except块中访问异常对象的相关信息,则可通过为异常对象声明变量来实现。当Python解释器决定调用某个except块来处理该异常对象时,会将异常对象赋值给except块后的异常变量,程序即可通过该变量来获得异常对象得相关信息。
所以的异常对象都包含了如下几个常用的属性和方法:
- args:该属性返回异常的错误编号和描述字符串。
- errno:该属性返回异常的错误编号。
- strerror:该属性返回异常的描述字符串。
- with_traceback():通过该方法可处理异常的传播轨迹信息。
def foo():try:fis=open('a.txt')except Exception as e:#访问异常的错误编号和详细信息print(e.args)#访问异常情况的错误编号print(e.errno)#访问异常的详细信息print(e.strerror)
foo()
"""
(2, 'No such file or directory')
2
No such file or directory
"""
从上面的运行结果可以看到,由于程序尝试打开的文件不存在,因此引发的异常错误编号为2,异常详细信息为:No such file or directory.
7.2.5 else块
在Python的异常处理流程中还可添加一个else块,当try块没有出现异常时,程序会执行else块。
s=input("请输入除数:")
try:result=20/int(s)print("20除以%s的结果时:%g"%(s,result))
except ValueError:print("值错误,您必须输入数值")
except ArithmeticError:print("算术错误,您不能输入0")
else:print("没有出现异常")
"""
请输入除数:a
值错误,您必须输入数值
"""
"""
请输入除数:0
算术错误,您不能输入0
"""
"""
请输入除数:4
20除以4的结果时:5
没有出现异常
"""
看到这里,可能会感到奇怪:既然只要try块没有异常时才会执行else块,那么直接把else块的代码放在try块的代码后面不就行了?
实际上大部分语言的异常处理都没有else块,它们确实是将else块的代码直接放在try块的代码后面的,因为对于大部分场景而言,直接将else块的代码放在try块的代码后面即可。
但Python的异常处理使用else块绝不是多余的语法,当try块没有异常,而else块有异常时,就能体现出else块的作用了。
def else_test():s=input("请输入除数:")result = 20 / int(s)print("20除以%s的结果时:%g" % (s, result))def right_main():try:print("try块的代码,没有异常")except:print("程序出现异常")else:#将else_test放在else块中else_test()
def wrong_main():try:print("try块的代码,没有异常")#将else_test放在try块的代码后面else_test()except:print("程序出现异常")
wrong_main()
right_main()
"""
try块的代码,没有异常
请输入除数:4
20除以4的结果时:5
try块的代码,没有异常
请输入除数:4
20除以4的结果时:5
"""
"""
try块的代码,没有异常
请输入除数:0
程序出现异常
try块的代码,没有异常
请输入除数:0
Traceback (most recent call last):File "", line 22, in <module>right_main()File "", line 13, in right_mainelse_test()File "", line 3, in else_testresult = 20 / int(s)~~~^~~~~~~~
ZeroDivisionError: division by zero"""
对比上面程序的两个输出结果,不难发现,放在else块中的代码所引发的异常不会被except块捕获。所以,如果希望某段代码的异常能被后面的except块捕获,那么就应该将代码放在try块的代码之后;如果希望某段代码的异常能向外传播(不被except块捕获),那么就应该将这段代码放在else块中。
7.2.6 使用finally回收资源
有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须被显式回收。
提示:Python的垃圾回收机制不会回收任何物理资源,只能回收堆内存中对象所占用的内存。
那么在哪里回收这些物理资源呢?在try块里回收,还是在except块中进行回收?假设程序在try块里进行资源回收,根据异常捕获流程图——如果try块的某条语句引发了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。如果在except块里进行资源回收,因为except完全有可能得不到执行,这将导致不能及时回收这些物理资源。
为了保证一定能回收在try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个except块被执行,甚至在try块或except块中执行了return语句,finally块总被执行。Python完整的异常处理语法结构如下:
try:#业务实现代码...
except SubException as e:#异常处理块1...
except SubException2 as e:#异常处理块2
...
else:#正常处理块
finally:#资源回收块...
在异常处理语法结构中,只要try块是必需的,也就是说,如果没有try块,则不能有后面的except块和finally块;except块和finally块都是可选的,但except块和finally至少出现其中之一,也可以同时出现;可以有多个except块,但捕获父类异常的except块应该位于捕获子类异常的except块的后面;不能只有try块,既没有except块,也没有finally块;多个except块必须位于try块之后,finally块必须位于所有的except块之后。
import os
def test():fis=Nonetry:fis=open('a.txt')except OSError as e:print(e.strerror)#return 语句强制方法返回return#os._exit(1)finally:#关闭磁盘if fis is not None:try:#关闭资源fis.close()except OSError as ioe:print(ioe.strerror)print("执行finally块里的资源回收!")
test()
"""
No such file or directory
执行finally块里的资源回收!
"""
上面程序在try块后增加了finally块,用于回收在try块中打开的物理资源。注意在程序的except块中有一条return语句,该语句强调方法返回。在通常情况下,一旦在方法里执行return语句,程序立即结束该方法;现在不会了,虽然return语句也强制方法结束,但一定先执行finally块的代码。
上面的运行结果表明在该方法返回之前执行了finally块的代码,将return换为exit(1)语句来退出Python解释器。
"""
No such file or directory
"""
上面运行结果表明finally块没有被执行。如果在异常处理代码中使用os._exit(1)语句来退出Python解释器,则finally块将失去执行的机会。
注意:除非在try块、except块中调用了退出Python解释器的方法,否则不管在try块、except块中执行怎样的代码,出现怎样的情况,情况处理的finally块总会被执行。调用sys.exit()方法退出程序不能阻止finally块的执行,这是因为sys.exit()方法本身就是通过引发SystemExit异常来退出程序的。
在通常情况下,不要再finally块中使用如return或raise等导致方法中止的语句,一旦在finally块中使用了return或raise语句,将会导致try块、except块中的return、raise语句失效。
def test():try:#因为finally块中包含了return语句#所有下面的return语句失去作用return Truefinally:return False
a=test()
print(a)
"""
False
"""
上面程序在finally块中定义了一条return False语句,这将导致try块中的return True失去作用。
如果Python程序在执行try块、except块时遇到了return或raise语句,这两条语句都会导致该方法立即结束,那么系统执行这两条语句并不会结束该方法,而是去寻找该异常处理流程中的finally块,如果没有找到finally块,程序执行return或raise语句,方法中止;如果找到finally块,系统立即执行finally块——只有当finally块执行完成后,系统才会再次调回来执行try块、except块里的return或raise语句;如果在finally块里也使用了return或raise等导致方法中止的语句,finally块已经中止了方法,系统将不会调回去执行try块、except块里的任何代码。
注意:尽量避免在finally块使用return或raise等导致方法中止的语句,否则可能出现一些奇怪的情况。
7.2.7 异常处理嵌套
正如前面的程序所示,在finally块中也包含了一个完整的异常处理流程,这种在try块、except块或finally块中包含完整的异常处理流程的情况被称为异常处理嵌套。
异常处理流程代码可以被放在任何能放可执行代码的地方,因此完整的异常异常处理流程即可被放在try块里,也可被放在except块里,还可被放在finally块里。
对异常处理嵌套的深度没有明确的限制,但通常没有必要使用超过两层的嵌套异常处理,使用层次太深的嵌套处理机制没有太大的必要,而且容易导致程序的可读性降低。
7.3使用raise引发异常
当程序出现错误,系统会自动引发异常。除此之外,Python也允许程序自行引发异常,自行引发异常使用raise语句来完成。
7.3.1引发异常
异常是一种很“主观”的说法,以下雨为例,假设大家约好大家明天去爬山郊游,如果第二天下雨了,这种情况会打破既定计划,就属于一种异常;但对于正在期盼天降甘霖的农民而言,如果第二天下雨了,他们正好随雨追肥,着完全正常。
很多时候,系统是否要引发异常,可能需要根据应用的业务需求来决定,如果程序中的数据,执行与既定的业务需求不符而产生的异常,必须由程序员来决定引发,系统无法引发这种异常。
如果需要在程序中自动引发异常,则应使用raise语句,raise语句有三种常用的用法:
- raise:单独一个raise。该语句引发当前上下文种捕获的异常(比如在except块中),或默认引发RuntimeError异常。
- raise异常类:raise后带一个异常类。该语句引发指定异常类的默认实例。
- raise异常对象:引发指定的异常对象。
上面三种用法最终都是要引发一个异常实例(即使指定的是异常类,实际上也是引发该类的默认实例),raise语句每次只能引发一个异常实例。
利用raise语句再次改写五子棋游戏中处理用户输入的代码。
try:#将用户输入的字符串以逗号(,)作为分隔符,分隔两个字符串x_str,y_str=inputStr.split(sep=',')#如果要下棋的点不为空if board[int(y_str)-1][(int (x_str)-1)]!='+':#引发默认的RuntimeError异常raise#把对应的列表元素为"·"board[int(y_str)-1][(int (x_str)-1)]="·"
except Exception as e:print(type(e))inputStr=input("您输入的坐标不合法,请重新输入,下棋坐标应以x、y的格式\n")continue
上面程序中使用raise语句来自行引发异常,程序认为当用户试图向一个已有棋子的坐标点下棋时就是异常。当Python解释器接收到开发者自行引发的异常时,同样会中止当前得到执行流,跳到该异常对应的except块,由该except块来处理该异常。也就是说,不管时系统自动引发的异常,还是程序员手动的异常,Python解释器对异常的处理没有任何差别。
即使时用户自行引发的异常,也可以使用try…except来捕获它。当然也可以不管它,让该异常向上(先调用者)传播,如果该异常传到Python解释器,那么程序就会中止。
def main():try:#使用try...except来捕获异常#此时即使程序出现异常,也不会传播给main函数mtd(3)except Exception as e:print("程序出现异常")#不使用try...except来捕获异常,异常会传播出来导致程序中止mtd(3)
def mtd(a):if a>0:raise ValueError("a的值大于0,不符合要求")
main()
"""
Traceback (most recent call last):File "y", line 13, in <module>main()File "", line 9, in mainmtd(3)File "", line 12, in mtdraise ValueError("a的值大于0,不符合要求")
ValueError: a的值大于0,不符合要求
程序出现异常
"""
上面程序可以看出,程序既可以在调用mtd(3)时使用try…except来捕获异常,这样该异常将会被except块捕获,不会传播给调用它的函数;也可以直接调用mtd(3),这样该函数的异常就会直接传播给它的调用函数,如果该函数也不处理该异常,就会导致程序中止。
提示:第二次调用mtd(3)引发的以“File”开头的三行输出,其实显示的就是异常的传播轨迹信息,也就是说,如果程序不对异常进行处理,Python默认会在控制台输出异常的传播轨迹信息。
7.3.2 自定义异常类
很多时候,程序可选择引发自定义异常,因为异常的类名通常也包括了该异常的有用信息。所以在引发异常时,应该选择合适的异常类,从而可以明确地描述异常情况。在这种情形下,应用程序常常需要引发自定义异常。
用户自定义异常都应该继承Exception基类或Exception的子类,在自定义异常时基本不需要书写更多的代码,只要指定自定义类型的父类即可。
class AuctionException(Exception):pass
上面程序创建了AuctionExcept异常类,该异常类不需要类体定义,因此使用pass语句作为占位符即可。
在大部分情况下,创建自定义异常类都采用上面程序相似的代码来完成,只需改变AuctionException异常的类名即可,让该类名可以准确地描述该异常类。
7.3.3 except和raise同时使用
在实际应用中对异常可能需要更复杂地处理方式——当一个异常出现时,单靠某个方法无法完成处理该异常,必须由几个方法协作才可完成该处理异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次引发异常,让该方法的调用者也能捕获到异常。
为了实现这种提供多个方法协作处理同一个异常的情形,可以在except块中结合raise语句来完成。
class AuctionException(Exception):pass
class AuctionTest:def __init__(self,init_price):self.init_price = init_pricedef bid(self,bid_price):d=0.0try:d=float(bid_price)except Exception as e:#此处指示简单地打印异常信息print("转换出异常:",e)#再次引发自定义异常raise AuctionException("竞拍加必须数值,不能包含其他符号!")if self.init_price>d:raise AuctionException("竞拍价格比起拍价格低,不允许竞拍")initPrice=d
def main():at=AuctionTest(20.4)try:at.bid("df")except AuctionException as ae:#再次获取bid()方法中的异常,并对该异常进行处理print("mian函数捕获的异常",ae)
main()
"""
转换出异常: could not convert string to float: 'df'
mian函数捕获的异常 竞拍加必须数值,不能包含其他符号!
"""
这种except和raise结合使用的情况在实际应用中非常适用。实际应用对异常处理通常分成两个部分:①应用后台需要通过日志来记录异常发生的详细情况;②应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法来实现,也就必须将except和raise结合使用。
如果程序需要将原始异常的详细信息直接传播出去,Python也允许用自定义异常对原始异常进行包装,只需要将上面的raise AuctionException("竞拍加必须数值,不能包含其他符号!")代码 该为如下形式:
raise AuctionException(e)
上面就是把原始异常e包装成了AuctionException异常,这种方式也被称为异常包装或异常转译。
7.3.4 raise不需要参数
正如前面所看到的,在使用raise语句时可以不带参数,此时raise语句正处于except块中,它将会自动引发上下文激活的异常;否则,通常默认引发RuntimeError异常。
class AuctionException(Exception):pass
class AuctionTest:def __init__(self,init_price):self.init_price = init_pricedef bid(self,bid_price):d=0.0try:d=float(bid_price)except Exception as e:#此处指示简单地打印异常信息print("转换出异常:",e)#再次引发自定义异常raiseif self.init_price > d:raise AuctionException("竞拍价格比起拍价格低,不允许竞拍")initPrice=d
def main():at=AuctionTest(20.4)try:at.bid("df")except Exception as ae:#再次获取bid()方法中的异常,并对该异常进行处理print("mian函数捕获的异常:",type(ae))
main()
"""
转换出异常: could not convert string to float: 'df'
mian函数捕获的异常: <class 'ValueError'>
"""
从输出结果来看,此时main()函数再次捕获了valueError——它就是在bid()方法中except块所捕获的原始异常。
7.4 Python的异常传播轨迹
异常对象提供了一个with_traceback用于处理异常的传播轨迹,查看异常的传播轨迹可追踪异常触发的源头,也可看到异常一路除法的轨迹。
class SelfException(Exception):pass
def main():firstMethod()
def firstMethod():secondMethod()
def secondMethod():thirdMethod()
def thirdMethod():raise SelfException("自定义异常信息")
main()
"""
Traceback (most recent call last):File "", line 10, in <module>main()File "", line 3, in mainfirstMethod()File "", line 5, in firstMethodsecondMethod()File "", line 7, in secondMethodthirdMethod()File "", line 9, in thirdMethodraise SelfException("自定义异常信息")
SelfException: 自定义异常信息
"""
上面程序中main()函数调用firstMethod(),firstMethod()函数调用secondMethod()函数,secondMethod()函数调用thirdMethod()直接引发了一个selfException异常。
异常从thirdMethod()函数开始触发,传到secondMethod()函数,再传到firstMethod()函数,最后传到main()函数,在main()函数止,这个过程就是Python的异常传播轨迹。
在实际应用程序的开发中,大多数复杂操作都会被分解成一系列函数或者方法调用。这是因为:为了具有更好的可重用性。会将每个重用的代码单元定义成函数或者方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个函数或方法来共同实现,在最终编程模型中,很多对象将通过一系列函数或方法调用来实现通信,执行任务。
所以,当应用程序运行时,经常会发生一系列函数或方法调用,从而形成“函数调用栈”。异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或者异常被处理后重新引发了新异常),异常就从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者在传给其调用者……直至最后传到Python解释器,此时Python解释器会中止该程序,并打印异常的传播轨迹信息。
很多人看到上图的异常提示信息,就会惊慌失措,他们以为程序出现了很多严重的错误,其实只是一个错误,系统提示那么多行信息,只不过是显示异常依次触发的轨迹。
其实上图的异常传播轨迹信息非常清晰——它记录了应用程序中执行停止的各个点。
最后一行信息详细显示了异常的类型和异常的详细信息。从这一行向上,逐个记录了异常发生源头、异常依次传播所经过的轨迹,并标明异常发生在哪个文件、哪一行、哪个函数处。
Python专门提供了traceback模块来处理异常传播轨迹,使用traceback可以方便地处理Python的异常传播途径。导入traceback模块之后,traceback提供了如下两个常用方法:
- traceback.print_exc():将异常传播轨迹信息输出到控制台或指定文件中。
- format_exc():将异常传播轨迹信息转换为字符串。
可以在有人感到好奇,从上面方法看不出它们到底处理哪个异常的传播轨迹信息。实际上常用的print_exc()是print_exc([limit[,file]])省略了limit、file两个参数的形式。而print_exc([limit[,file]])的完整形式是print_exception(etype,value,tb[,limit[,file]]),在完整形式中,起那面三个参数用于分别指定异常的如下信息:
- etype:指定异常类型。
- value:指定异常值。
- th:指定异常的traceback信息。
当程序处于except块中时,该except块所捕获的异常信息可通过sys对象来获取,其中sys.exc_type、sys.exc_value、sys.exc_trackback就代表当前except块内的异常类型、异常值和异常传播轨迹。
简单来说,print_exc([limit[,file]])会自动处理当前except块所捕获的异常。该方法还涉及两个参数:
- limit:用于限制显示异常传播的层次,比如函数A调用函数B,函数B发生了异常,如果指定limit=1,则只显示函数A里面发生的异常。如果不设置limit参数,则默认全部显示。
- file:指定将异常传播轨迹信息输出到指定文件中。如果不指定该参数,则默认输出到控制台,借助于traceback模块的帮助,我们可以使用except块捕获异常,并在其中打印异常传播信息。
#导入traceback模块
import traceback
class SelfException(Exception):pass
def main():firstMethod()
def firstMethod():secondMethod()
def secondMethod():thirdMethod()
def thirdMethod():raise SelfException("自定义异常信息")
try:main()
except:#捕获异常,并将异常传播信息输出到控制台traceback.print_exc()#捕获异常,并将异常传播信息输出到指定文件中traceback.print_exc(file=open('log.txt','a'))
7.5 异常处理规则
前面介绍了很多异常处理的优势、便捷之处。成功的异常处理应该实现如下4个目标:
- 使程序代码混乱最小化。
- 捕获并保留诊断信息。
- 通知合适的人员。
- 采用合适的方式结束异常活动。
7.5.1不要过度使用异常
不可否认,Python的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要表现在两个方面:
- 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地引发异常来代替所以的错误处理。
- 使用异常处理来代替流程控制,
熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地引发异常。实际上这样做是不对的,对于完全已知的错误和普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对于外部的、不能确定和预知的运行时错误才使用异常。
对比前面五子棋游戏中,处理用户输入坐标点已有棋子的两种方式。
如果用户试图下棋的坐标已有棋子:
#如果要下棋的点不为空
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块来捕获该异常,所以运行效率要差一些。而且用户下棋重复这个错误完全是可预料的,所以程序完全可以针对该错误提供相应的处理,而不是引发异常。
必须指出:异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务处理逻辑判断。
另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。
#定义一个字符串列表
my_list=['Hello','Python','Spring']
#使用异常处理来遍历arr数组的每个元素
try:i=0while True:print(my_list[i])i+=1
except:pass
"""
Hello
Python
Spring
"""
运行上面程序确实可以实现遍历my_list列表的功能,但这种写法可读性较差,而且运行效率不高。程序完全又能例避免产生IndexError异常,程序“故意”制造这种异常,然后使用except块来捕获该异常,这是不应该的。将程序改为如下形式肯定要好很多:
#定义一个字符串列表
my_list=['Hello','Python','Spring']
i=0
while i<len(my_list):print(my_list[i])i+=1
"""
Hello
Python
Spring
"""
注意:异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制,对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。
7.5.2 不要使用过于庞大地try块
很多人一开始学地十号很喜欢在try块里放置大量地代码,这看上去很“简单”,但这种“简单”只是一种假象,只是在编写程序时看上去比较简单。但因为try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加了。
而且当try块过于庞大时,就难免在try块后紧跟大量的except块才可以针对不同的异常提供不同的处理逻辑。在同一个try快后紧跟大量的except块则需要分析它们之间的逻辑关系,反而增加了编程复杂度。
正确的做法是,把大块的try块分隔成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
7.5.3 不要忽略捕获的异常
不要忽略异常!既然已经捕获到异常,那么except块理应做些有用的事情——处理并修复异常,except块整个为空,或者仅仅打印简单的异常信息都是不妥的!
except块为空就是假装不知道甚至瞒天过海,这是最可怕的事情——程序出了错误,所有人都看不到任何异常,但整个应用可能已经彻底坏了。仅在except块里打印异常传播信息稍微好一点,但仅仅比空白多了几行异常信息。通常建议对异常采取适当措施,比如:
- 处理异常:对异常进行合适的修复,然后绕过异常发生的地方继续进行;或者用别的数据进行修复,以代替期望的方法返回值;或者提示用户重新操作……总之,程序应该尽量修复异常,使程序恢复正常。
- 重新引发新异常。把在当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新传给上层调用者。
- 在合适的层处理异常,如果当前层不清楚如何处理异常,就不要在当前层使用except块来捕获该异常,让上层调用者来处理该异常。