一、捕获异常
一旦发生异常,程序就会终止,这是非常糟糕的事情,这种糟糕体现在两方面
- 即便发生了异常,如果业务上可以忽略它,那么程序应当继续执行
- 程序终止,使得异常的信息没有被保留下来,不利于问题的分析和总结
为了提高程序的健壮性和解决问题,可以将异常捕获,根据业务要求来做对应的处理。
1、try except
#句式1,捕获到异常后,执行指定动作,注意这里不指定异常类型,会捕获所有的异常,
try:
可能出错的代码放在try下运行,一旦出错,就会被捕获到
except:
如果捕获到异常,就会执行这里except下的语句,这里可以用pass,表示什么都不做,但不能省略
#例1
try:
print(a*3) #变量a没有提前声明定义,所以该语句实际上会有异常,异常类型是NameError
except: #捕获到异常后,要不要报异常信息,要不要执行其他处理,都要自行决定
print(1) #这里要执行的其他处理动作是打印1
print("如果打印本信息,说明发生异常后,程序继续执行了")
#执行结果是:
#1
#如果打印本信息,说明发生异常后,程序继续执行了
#句式1的特殊之处是,except后面直接是冒号,没有带任何异常类型,这意味将会捕获程序代码中的所有异常类型,包括键盘中断和程序退出请求(用sys.exit()就无法退出程序了,因为异常被捕获了),官方不推荐使用,你也要尽量不要用,除非你很明确自己想做什么。一般来说,懒人可以用下面的句式2
#另外要注意,try不能单独使用,但是try不一定要搭配except,try还可以搭配finally,后面会讲;except下面必须要有语句,不能为空。
#句式2,捕获到异常后,执行指定动作,注意这里指定异常类型后,只捕获指定类型的异常
try:
可能出错的代码放在try下运行,一旦出现except后跟的异常类型1这种异常,就会被捕获到
except <异常类型1>:
如果捕获到异常类型1,就会执行这里except下的语句
#例1
try:
print(a*3) #变量a没有提前声明定义,所以该语句实际上会有异常,异常类型是NameError
except NameError: #捕获到异常后,要不要报异常信息,要不要执行其他处理,都要自行决定
print(1) #这里要执行的其他处理动作是打印1
print("如果打印本信息,说明发生异常后,程序继续执行了")
#执行结果是:
#1
#如果打印本信息,说明发生异常后,程序继续执行了
#例2,如果except后面跟的不是NameError异常,而是其他的呢?
try:
print(a*3) #变量a没有提前声明定义,所以该语句实际上会有异常,异常类型是NameError
except ZeroDivisionError: #捕获到异常后,要不要报异常信息,要不要执行其他处理,都要自行决定
print(1) #这里要执行的其他处理动作是打印1
print("如果打印本信息,说明发生异常后,程序继续执行了")
#执行的结果是:
Traceback (most recent call last):
File "D:\PyGuide\错误和异常\zy_exception.py", line 4, in <module>
print(a*3) #变量a没有提前声明定义,所以该语句实际上会有异常,异常类型是NameError
NameError: name 'a' is not defined
#你会发现报异常信息了,程序不能忽略异常继续执行。因为你只指定了ZeroDivisionError异常,但程序没有在这段程序里发现该异常,而是发现了NameError异常,也就是说捕获失败,捕获失败相当于没有捕获,就会报错。
#如果这里把ZeroDivisionError换成Exception,程序会如同例1一样执行并输出两行内容。原因是NameError类型派生自Exception,相当于Exception包含NameError。如果你能明确的知道该处可能出现的异常类型最好,如果不是很清楚,那么就可以用Exception类型,因为我们知道,大部分的异常类型都派生自Exception,常规的错误都能捕捉到,从而最大程度上让程序继续执行,而不是中断。
#例3,通过指定正确的异常类型,或者相对宽泛的异常类型(Exception),可以让程序捕捉到异常,但我们怎么知道发生了哪个异常呢?
try:
print(a*3) #变量a没有提前声明定义,所以该语句实际上会有异常,异常类型是NameError
except NameError as e: # as e 代表异常信息,当然,这里可以不用e,用其他名字都可以,但是约定成俗用e
print(1) #这里要执行的其他处理动作是打印1
print(e) #这里就是报异常信息,必须要写上这句,否则不会报
print("如果打印本信息,说明发生异常后,程序继续执行了")
#注意,在 Python 2.x 的早期版本中,除了使用 as e 这个格式,还可以将其中的 as 用逗号(,)代替。
#句式4,捕获到异常后,执行指定动作,注意这里指定异常类型后,只捕获指定类型的异常,但是你可以同时指定多种异常类型
try:
可能出错的代码放在try下运行,一旦出现except后跟的异常类型,就会被捕获到
except <异常类型1>,<异常类型2>,<异常类型3>……:
如果捕获到异常类型1或2或3,就会执行这里except下的语句
#例1:当代码可能会引发多种异常,都想进行捕获时,那就要将多个异常类放在一个元组里,写在except关键字的后面
try:
print(a*3) #变量a没有提前声明定义,所以该语句实际上会有异常,异常类型是NameError
print(6/0) #0不能做除数,或者说0不能做分母,所以该语句实际上会有异常,异常类型是ZeroDivisionError
except (NameError,ZeroDivisionError) as e: # as e 代表异常信息,并非必须要有,e是约定成俗的名字,可以用其他名字
print(1) #这里要执行的其他处理动作是打印1
print(e) #这里就是报异常信息,必须要写上这句,否则不会报
print("如果打印本信息,说明发生异常后,程序继续执行了")
#如例1,无论try下的代码出现NameError异常还是出现ZeroDivisionError异常,程序都会捕获到,执行except下的代码后,继续运行。另外注意,print(e)只会报第一个遇到的异常,不会把所有的异常都报出来。除了这种句式可以捕获多种异常类型外,还有一种句式可以,效果一样。
#句式5,捕获到异常后,执行指定动作,注意这里指定异常类型后,只捕获指定类型的异常,但是你可以同时指定多种异常类型
try:
可能出错的代码放在try下运行,一旦出现except后跟的异常类型这种异常,就会被捕获到
except <异常类型1>:
如果捕获到异常类型1,就会执行这里except下的语句
except <异常类型2>:
如果捕获到异常类型2,就会执行这里except下的语句
except <异常类型3>:
如果捕获到异常类型3,就会执行这里except下的语句
#这里要注意,程序只会执行一个except子句,比如说,try子句中,第一句是异常类型1,第二句是异常类型2,那么只会执行except <异常类型1>下面的子句,不会执行 except<异常类型2>下面的子句,这有点类似 if……elif的分支选择。按照这个道理,即便你可以在每个except语句的后面 加上 as e ,然后在每个except语句的下面,加上print(e),程序也不会把所有的异常类型都打印出来,它只会打印遇到的第一个异常类型信息。另外注意,多个异常类型下,程序先捕获哪个,取决于哪个现在try子句中出现,不取决于except语句的顺序
#该种多个except子句的异常处理语法的规则是:
#执行 try 下的语句,如果引发异常,则执行过程会跳到第一个 except 语句。
#如果第一个 except 中定义的异常与引发的异常匹配,则执行该 except 中的语句。
#如果引发的异常不匹配第一个 except,则会搜索第二个 except,允许编写的 except 数量没有限制。
#如果所有的 except 都不匹配,则异常会传递到下一个调用本代码的最高层 try 代码中。
通过以上五个句式,我们可以把try...except...句式进行归纳:
其基本语法结构如下所示:
try:
可能产生异常的代码块
except [ (Error1, Error2, ... ) [as e] ]:
处理异常的代码块1
except [ (Error3, Error4, ... ) [as e] ]:
处理异常的代码块2
except [Exception]:
处理其它异常
该格式中,[] 括起来的部分可以使用,也可以省略。其中各部分的含义如下:
- (Error1, Error2,...) 、(Error3, Error4,...):其中,Error1、Error2、Error3 和 Error4 都是具体的异常类型。显然,一个 except 块可以同时处理多种异常。
- [as e]:作为可选参数,表示给异常类型起一个别名 e,这样做的好处是方便在 except 块中调用异常类型并输出。
- [Exception]:该异常类型包括了常规的大部分异常情况,其通常用在最后一个 except 块中,防止有没捕获到的异常。
除了句式外,我们把原理也归纳为几个要点:
-
首先,执行 try 子句 (try 和 except 关键字之间的(多行)语句,也就是你怀疑可能会出错的代码部分)
-
如果没有异常发生,则完成 try 语句的执行,跳过 except 子句 ,然后继续执行程序其他代码。
-
如果在执行try 子句时发生了异常,则跳过该子句中剩下的部分。然后,如果异常的类型和 except 关键字后面的异常匹配,则执行 except 子句,注意,这里不会说执行完except子句后再跳回try子句,把try中异常语句下面的部分再执行完,程序执行完except子句后,会直接往后执行程序其他部分。
-
如果发生的异常和 except 子句中指定的异常不匹配,则将其传递到外部的 try 语句中(可以简单理解为,try except可能是嵌套在另一个try except中,内部的处理不了,就给外部处理,也就是传递到外部的try语句中);如果没有找到处理异常的代码,则它是一个 未处理异常,执行将停止并显示异常类型信息
-
一个 try 语句可能有多个 except 子句,以指定不同异常的处理程序,但是程序最多会执行一个处理程序,不会说,只要try子句中有这个异常类型就执行except后的句子,因为一旦遇到异常,执行该异常的except后,直接退出try except语句,然后往后执行程序。
-
不管程序代码块是否处于 try 块中,只要执行该代码块时出现了异常,系统都会自动生成对应类型的异常。如果此段程序没有用 try 包裹,又或者没有为该异常配置处理它的 except 块, Python 解释器将无法处理该异常,程序就会停止运行;反之,如果程序发生的异常经 try 捕获并由 except 处理完成,则程序可以继续执行。
2、try except else
在原本的try except结构的基础上,python异常处理机制还提供了一个 else 块,也就是原有 try except 语句的基础上再添加一个 else 块,即try except else结构,else 必须和 try except 搭配使用。
使用 else 包裹的代码,只有当 try 块没有捕获到任何异常时,才会得到执行;反之,如果 try 块捕获到异常,在调用对应的 except 处理完异常后,也不会执行else 块中的代码。
try:
result = 20 / int(input('请输入除数:'))
print(result)
except ValueError:
print('必须输入整数')
except ArithmeticError:
print('算术错误,除数不能为 0')
else:
print('没有出现异常')
print("继续执行")
可以看到,在原有 try except 的基础上,我们为其添加了 else 块。现在执行该程序:
请输入除数:4
5.0
没有出现异常
继续执行
如上所示,当我们输入正确的数据时,try 块中的程序正常执行,Python 解释器执行完 try 块中的程序之后,会继续执行 else 块中的程序,继而执行后续的程序。
读者可能会问,既然 Python 解释器按照顺序执行代码,那么 else 块有什么存在的必要呢?直接将 else 块中的代码编写在 try except 块的后面,不是一样吗?当然不一样,现在再次执行上面的代码:
请输入除数:a
必须输入整数
继续执行
可以看到,当我们试图进行非法输入时,程序会发生异常并被 try 捕获,Python 解释器会调用相应的 except 块处理该异常。但是异常处理完毕之后,Python 解释器并没有接着执行 else 块中的代码,而是跳过 else,去执行后续的代码。
也就是说,else 的功能,只有当 try 块捕获到异常时才能显现出来。在这种情况下,else 块中的代码不会得到执行的机会。而如果我们直接把 else 块去掉,将其中的代码编写到 try except 的后面:
try:
result = 20 / int(input('请输入除数:'))
print(result)
except ValueError:
print('必须输入整数')
except ArithmeticError:
print('算术错误,除数不能为 0')
print('没有出现异常')
print("继续执行")
程序执行结果为:
请输入除数:a
必须输入整数
没有出现异常
继续执行
可以看到,如果不使用 else 块,try 块捕获到异常并通过 except 成功处理,后续所有程序都会依次被执行,你并不知道有没有出现异常,而通过else子句你就可以判断到底是没有异常,还是异常被except处理了,这一点很重要。
3、try except finally
python异常处理机制还提供了一个 finally 语句,通常用来为 try 块中的程序做扫尾清理工作。
注意,和 else 语句不同,finally 只要求和 try 搭配使用,而至于该结构中是否包含 except 以及 else,对于 finally 不是必须的(else 必须和 try except 搭配使用)。
在异常处理机制中,finally 语句的功能是:无论 try 块是否发生异常,最终都要进入 finally 语句,并执行其中的代码块。
基于 finally 语句的这种特性,在某些情况下,当 try 块中的程序打开了一些物理资源(文件、数据库连接等)时,由于这些资源必须手动回收,而回收工作通常就放在 finally 块中。
Python 垃圾回收机制,只能帮我们回收变量、类对象占用的内存,而无法自动完成类似关闭文件、数据库连接等这些的工作。
当然,你可能会问,回收这些物理资源,必须使用 finally 块吗?当然不是,但使用 finally 块是比较好的选择。首先,try 块不适合做资源回收工作,因为一旦 try 块中的某行代码发生异常,则其后续的代码将不会得到执行;其次 except 和 else 也不适合,它们都可能不会得到执行。而 finally 块中的代码,无论 try 块是否发生异常,该块中的代码都会被执行。
举个例子:
try:
a = int(input("请输入 a 的值:"))
print(20/a)
except:
print("发生异常!")
else:
print("执行 else 块中的代码")
finally :
print("执行 finally 块中的代码")
运行此程序:
请输入 a 的值:4
5.0
执行 else 块中的代码
执行 finally 块中的代码
可以看到,当 try 块中代码未发生异常时,except 块不会执行,else 块和 finally 块中的代码会被执行。
再次运行程序:
请输入 a 的值:a
发生异常!
执行 finally 块中的代码
可以看到,当 try 块中代码发生异常时,except 块得到执行,而 else 块中的代码将不执行,finally 块中的代码仍然会被执行。
finally 块的强大还远不止此,即便当 try 块发生异常,且没有except 处理异常时,finally 块中的代码也会得到执行。例如:
try:
#发生异常
print(20/0)
finally :
print("执行 finally 块中的代码")
程序执行结果为:
执行 finally 块中的代码
Traceback (most recent call last):
File "D:\python3.6\1.py", line 3, in <module>
print(20/0)
ZeroDivisionError: division by zero
可以看到,当 try 块中代码发生异常,导致程序崩溃时,在崩溃前 Python 解释器也会执行 finally 块中的代码。
二、获取有关异常的详细信息
通过前面的学习,我们已经可以捕获程序中可能发生的异常,对其进行处理,并能够输出异常的相关信息,但是你发现,我们通过print(e)这种方式输出的信息特别少,如何获取更多呢?其实,每种异常类型都提供了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的更多相关信息:
- args:返回异常的错误编号和描述字符串;
- str(e):返回异常信息,但不包括异常信息的类型;
- repr(e):返回较全的异常信息,包括异常信息的类型和描述字符串。
举个例子:
try:
1/0
except Exception as e:
# 访问异常的错误编号和详细信息
print(e.args)
print(str(e))
print(repr(e))
输出结果为:
('division by zero',)
division by zero
ZeroDivisionError('division by zero',)
当然了,即便是经过如上的处理,你还是发现不如python直接报错给的信息多,怎么办?如果你确实需要更详细的错误信息帮助排查,那你可以可以使用 traceback 等模块,后面我们会讲到。