Python中的异常处理

在编程过程中,会出现各种“问题”,“问题”大致分为两种:

  • 错误
  • 异常

12.1 异常

12.1.1 异常的概念

异常是程序运行过程中产生的一种事件,该事件会打乱程序的正常流程。可以说,异常就是一种意外,指程序没有按照正常或期望的方式执行
当异常产生时,会创建一个相关异常类的对象,该对象含有异常的相关信息。异常产生时,会在异常的上下文中寻找异常处理程序,如果没有异常处理程序,则异常产生之后的语句将不会得到执行。该异常会向上传播。传播的方式为:

  • 如果异常在函数中产生,则会传播给函数的调用端。
  • 如果异常在模块中(函数外)产生,则会传播给导入该模块的模块。
  • 如果传播到作为脚本运行的模块,还未处理该异常,则会将异常传播给解释器,此时,整个线程终止执行。在控制台会打印出异常的相关信息与堆栈的调用轨迹。轨迹是按照方法调用的顺序或模块引用的顺序打印,离异常发生地最近的方法或模块会最后打印。

12.1.2 常见异常类型

异常命名惯例,以Error结尾。

BaseException
Exception
1.	ZeroDivisionError
2.	NameError
3.	TypeError
4.	AtrributeError
5.	Indentation
6.	IndexError
7.	UnboundLocalError
8.	AssertionError
9.	ModuleNotFoundError
10.	KeyError
11.	RecursionError
12.	StopIteration
13.	ValueError
14.	SyntaxError

12.2 捕获异常

12.2.1 try…except

在Python中,可以使用try-except的语法来捕获异常,我们可以将其称为异常处理程序。其语法格式如下:

try:
    可能产生异常的程序
except 异常类型1:
    恢复措施
except 异常类型2:
    恢复措施
……
except 异常类型n:
    恢复措施

其中,try用来执行可能会产生异常的程序,而except用来捕获try中产生的异常,用来执行一些恢复或补救操作,或者给出错误的提示信息等。这分为三种情况:

  • try语句块没有产生异常。
    此时try语句块全部执行完毕,不会执行任何except分支(因为没有异常),然后继续执行try-except之后的语句。
  • try语句块中产生异常,except捕获了该异常。
    此时会创建一个相关异常类的对象,异常之后的语句将不会得到执行。程序会跳转到except分支,从上到下依次使用异常类对象与每个except中的异常类型进行匹配(判断异常对象是否为except中异常类型的实例),哪一个except分支匹配成功,则执行哪个except分支,成功将异常捕获。try-except之后的语句正常执行。对于except分支,至多只会执行一个(执行先匹配成功的分支)。
  • try语句块中产生异常,但是except没有捕获该异常。
    当try中产生异常时,try异常之后的语句不会得到执行。程序跳转到except分支进行异常匹配。没有匹配成功,则表示没有捕获,该异常继续存在,而try-except之后的语句不会得到执行,异常会继续向上传播(传播给函数的调用端或模块的引入处)。

获取异常对象
我们可以使用except捕获异常,同时,我们也能够使用as语法获取try中产生的异常对象。语法格式为:

try:
    可能产生异常的代码
except 异常类型 as 变量:
    处理异常代码

当异常匹配成功时,我们就会将try中产生的异常对象赋值给as变量名,然后,我们就可以在except中使用变量名来访问异常对象了。

try:
    print(5 / 0)
except ZeroDivisionError as e:
    # 通过异常对象的args属性获取异常对象构造器的参数。
    print(e.args)

异常对象的args属性返回一个元组类型,其中存放异常对象创建时,传递给构造器中的参数值。

12.2.2 捕获多个异常

因为在try语句块中,可能产生不止一种异常,故我们会使用多个except分支来捕获这些可能产生的异常。如果多个异常类之间没有继承关系时,except分支的顺序不是十分重要,但是,当异常类之间存在异常关系时,就一定要将子类放在前面,父类放在后面。因为子类异常对象也是父类异常类的实例,如果将父类分支放在子类分支之前,则就算try中产生子类异常,也会先由父类分支率先捕获,子类分支永远都没有机会执行,这就失去了意义。
所以,在捕获异常时,except分支应该按照从特殊性(子类)到一般性(父类)的方式排序。
except还有一种语法,就是不指定异常类型,此时表示捕获所有异常类型。例如:

try:
    可能产生异常的代码
except:				# 没有指定异常类型,会捕获所有的异常。
    pass

因为捕获所有的异常类型是最广泛的(最具有一般性),所以,如果使用这种方式,则必须将该except置于最后(作为最后一条except分支)。
同时捕获多个异常
当try中产生了两种(或更多)的异常,而多种异常的处理方式又完全相同时,我们使用多条except分支,会造成代码的重复。
此时,我们可以使用一条except分支,同时捕获多个异常来代替。

try:
    # 操作
except (IndexError, KeyError):
    print("提供值不合法,获取失败!")

这样,无论try中产生IndexError还是KeyError,except分支都可以进行捕获。
也许大家会有这样的想法,这种捕获多个异常有什么用呢,使用不指定异常类型的except岂不是更好,能捕获所有异常,可谓“万事通用”。

try:
    # 操作
except:
    print("提供值不合法,获取失败!")

但是,如果这样做,就很可能会捕获预期之外的异常,从而掩埋真正的问题,令错误不易排查。因此,如果能够捕获更加具体明确的异常类型,我们最好不要使用更加通用一般的异常类型代替。

12.2.3 else

try-except还可以跟随可选的else语句。语法为:

try:
    ……
except 异常类型:
    ……
else:
    ……

当try中没有产生异常时,就会执行else分支,否则,不执行else分支。

12.2.4 finally

try-except还可以带上一个可选的finally。如果同时存在else,则finally必须处于else的后面,其实,except,else,与finally都是可选的。但是,except与finally二者不能同时缺失,即二者至少要存在一个。
finally会在try-except-else之后得到执行(如果存在except或else的话),且一定会得到执行。即无论try是否产生异常,也无论except是否捕获try中产生的异常,finally终将会得到执行。考虑到finally总是可以执行的特征,我们往往会在finally中执行一些清理的工作。例如,我们在try中申请了一些系统资源(文件读写,数据库连接等),就可以在finally中进行资源释放,从而不会造成资源泄露(资源申请,但没有释放)。如果将释放语句写在try中,则一旦在释放之前产生异常,则资源释放语句就不会得到执行。
finally总是会执行的尝试。当try语句体中尝试返回一个变量时,如果在finally中去修改该变量的值,不会影响到返回值的结果(返回的还是修改之前的值)。

12.3 手动抛出异常

我们也可以自行创建一个异常类型的对象,然后将其抛出。这与之前产生异常的行为是相同的。也许大家为问:异常是一种意外,是我们应该极力去避免的,为什么还要主动去“创建”异常呢?
我们在编写程序时,可能会接收调用端传递过来的值,但是,我们无法保证调用端传递的值永远是正确的。例如,在注册用户的时候,提供年龄信息,如果输入了负值,这明显是不正确的。我们可以使用if来进行判断:

def register(age):
    if age > 0:
        注册操作

我们进行了合理的判断,但问题是,如果age小于等于0,则程序会“保持沉默”,没有任何信息提示。这对于调用端来说,可能未必是一件好事。如果这个行为很重要,那此时产生一个异常才是更合理的结果。因为使用if判断的形式,纵然调用端传递非法的值,我们最多也只是不执行操作,但却没有什么有效的措施能够牵制调用端,或是以一种强力的方式去通知调用端已经犯了较为严重的错误。尽管,我们可以使用print函数打印提示信息,但调用端很可能会忽略这些信息。
相反,抛出异常的方式则不同。异常可以向上传播,如果调用端没有明确对异常进行处理,将会导致当前的线程终止,同时显示异常的错误信息,这就可以引起调用端足够的重视,而不至于掩盖程序的bug(软件漏洞)。
我们可以创建一个异常对象,使用raise抛出,语法为:
raise 异常类或异常对象
例如:

raise Exception("产生异常")
raise Exception
raise后面跟随异常类,则相当于是调用其无参的构造器,因此,后者相当于:
raise Exception()

raise后面必须是一个有效的异常类(BaseException类型或其子类型)或对应异常类的对象,如果是其他类型,将会产生错误。
这里还有一个问题,既然调用端可以采用try-except处理异常,那为什么不在register方法里面捕获可能的异常呢?这样不就方便所有的调用端了吗?原因如下:

  • 如果在方法内捕获异常,则异常消失,就不能给调用端一个有效的提醒。
  • 如果在方法内捕获异常,则需要对异常产生时做出处理。

13.4. 自定义异常

之前,我们使用的是内建的异常类型ValueError,但是,ValueError是Python内建的异常类型,其具有自身特殊的应用场景,如果我们使用系统内建的异常类型,容易造成混淆。例如,我们在年龄不合法时抛出该异常,而数值转换失败时,移位运算右侧操作数为负数时也会产生该异常,这就不便于我们定位与排查问题。因此,我们可以自定义异常类型,用在我们需要的场景,这样就可以避免与内建异常类型相互干扰。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值