1. 前言
在自己摸索开发的过程中,程序运行时难免会碰到一些错误,尤其当前后端交互时出现各种资源问题,例如404,500等,在写前端代码的时候,就深知异常处理机制是十分重要的,因此在学习python时,有必要去学习一下Python程序的异常处理机制。
当Python程序遇到一些错误,例如除数为 0、年龄为负数、数组下标越界等,这些错误如果不能发现并加以处理,很可能会导致程序崩溃。处理异常机制的存在,可以让我们捕获并处理这些错误,让程序继续沿着一条不会出错的路径执行。
可以简单的理解异常处理机制,就是在程序运行出现错误时,让 Python 解释器执行事先准备好的除错程序,进而尝试恢复程序的执行。
借助异常处理机制,甚至在程序崩溃前也可以做一些必要的工作,例如将内存中的数据写入文件、关闭打开的文件、释放分配的内存等。
Python 异常处理机制会涉及 try、except、else、finally 这 4 个关键字,同时还提供了可主动使程序引发异常的 raise 语句。
2. 常见的异常类型
编写程序时遇到的错误大致分为两类:语法错误 和 运行时错误。
2.1 语法错误
语法错误,也就是解析代码时出现的错误。当代码不符合 Python 语法规则时,Python解释器在解析时就会报出 SyntaxError 语法错误,与此同时还会明确指出最早探测到错误的语句。例如:
xxxxxxxxxx11print "Hello,World!"我们知道,Python 3 已不再支持上面这种写法,所以在运行时,解释器会报如下错误:
xxxxxxxxxx11SyntaxError: Missing parentheses in call to 'print'语法错误多是开发者疏忽导致的,属于真正意义上的错误,是解释器无法容忍的,因此,只有将程序中的所有语法错误全部纠正,程序才能执行。
2.2 运行时错误
运行时错误,即程序在语法上都是正确的,但在运行时发生了错误。例如:
xxxxxxxxxx11a = 1/0上面这句代码的意思是“用 1 除以 0,并赋值给 a 。因为 0 作除数是没有意义的,所以运行后会产生如下错误:
xxxxxxxxxx51>>> a = 1/02Traceback (most recent call last):3 File "<pyshell#2>", line 1, in <module>4 a = 1/05ZeroDivisionError: division by zero以上运行输出结果中,前两段指明了错误的位置,最后一句表示出错的类型。在 Python 中,把这种运行时产生错误的情况叫做异常(Exceptions)。
这种异常情况还有很多,常见的几种异常情况如表 1 所示:
| 异常类型 | 含义 | 实例 |
|---|---|---|
| AssertionError | 当 assert 关键字后的条件为假时,程序运行会停止并抛出 AssertionError 异常 | >>> assert 0>1 AssertionError |
| AttributeError | 当试图访问的对象属性不存在时抛出的异常 | >>> demo_list = ['ChenMo'] >>> demo_list.len AttributeError: 'list' object has no attribute 'len' |
| IndexError | 索引超出序列范围会引发此异常 | >>> demo_list = ['ChenMo'] >>> demo_list[3] IndexError: list index out of range |
| KeyError | 字典中查找一个不存在的关键字时引发此异常 | >>> demo_dict={"name":"ChenMo"} >>> demo_dict["age"] KeyError: 'age' |
| NameError | 尝试访问一个未声明的变量时,引发此异常 | >>> ChenMo NameError: name 'ChenMo' is not defined |
| TypeError | 不同类型数据之间的无效操作 | >>> 1+'ChenMo' TypeError: unsupported operand type(s) for +: 'int' and 'str' |
| ZeroDivisionError | 除法运算中除数为 0 引发此异常 | >>> a = 1/0 ZeroDivisionError: division by zero |
提示:表中的异常类型不需要记住,只需简单了解即可。
当一个程序发生异常时,代表该程序在执行时出现了非正常的情况,无法再执行下去。默认情况下,程序是要终止的。如果要避免程序退出,可以使用捕获异常的方式获取这个异常的名称,再通过其他的逻辑代码让程序继续运行,这种根据异常做出的逻辑处理叫作异常处理。
开发者可以使用异常处理全面地控制自己的程序。异常处理不仅仅能够管理正常的流程运行,还能够在程序出错时对程序进行必的处理。大大提高了程序的健壮性和人机交互的友好性。
3. try except
3.1 基本使用
Python 中,用try except语句块捕获并处理异常,其基本语法结构如下所示:
xxxxxxxxxx81try:2 # 可能产生异常的代码块3except [ (Error1, Error2, ... ) [as e] ]:4 # 处理异常的代码块15except [ (Error3, Error4, ... ) [as e] ]:6 # 处理异常的代码块27except [Exception]:8 # 处理其它异常该格式中,[] 括起来的部分可以使用,也可以省略。其中各部分的含义如下:
- (Error1, Error2,…) 、(Error3, Error4,…):其中,Error1、Error2、Error3 和 Error4 都是具体的异常类型。显然,一个 except 块可以同时处理多种异常。
- [as e]:作为可选参数,表示给异常类型起一个别名 e,这样做的好处是方便在 except 块中调用异常类型(后续会用到)。
- [Exception]:作为可选参数,可以代指程序可能发生的所有异常情况,其通常用在最后一个 except 块。
从try except的基本语法格式可以看出,try 块有且仅有一个,但 except 代码块可以有多个,且每个 except 块都可以同时处理多种异常。
当程序发生不同的意外情况时,会对应特定的异常类型,Python 解释器会根据该异常类型选择对应的 except 块来处理该异常。
try except 语句的执行流程如下:
- 首先执行 try 中的代码块,如果执行过程中出现异常,系统会自动生成一个异常类型,并将该异常提交给 Python 解释器,此过程称为捕获异常。
- 当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为处理异常。如果 Python 解释器找不到处理异常的 except 块,则程序运行终止,Python 解释器也将退出。
事实上,不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现了异常,系统都会自动生成对应类型的异常。但是,如果此段程序没有用 try 包裹,又或者没有为该异常配置处理它的 except 块,则 Python 解释器将无法处理,程序就会停止运行;反之,如果程序发生的异常经 try 捕获并由 except 处理完成,则程序可以继续执行。
xxxxxxxxxx151try:2 a = int(input("输入被除数:"))3 b = int(input("输入除数:"))4 c = a / b5 print("您输入的两个数相除的结果是:", c )6except (ValueError, ArithmeticError):7 print("程序发生了数字格式异常、算术异常之一")8except :9 print("未知异常")10print("程序继续运行")11
12# 程序运行结果为:13# > 输入被除数:a14# > 程序发生了数字格式异常、算术异常之一15# > 程序继续运行由于 try 块中引发了异常,并被 except 块成功捕获,因此程序才可以继续执行,才有了“程序继续运行”的输出结果。
3.2 获取特定异常的有关信息
由于一个 except 可以同时处理多个异常,那么就需要知道当前处理的到底是哪种异常,毕竟总不能每个异常都写一个 except。
每种异常类型都提供了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的相关信息:
- args:返回异常的错误编号和描述字符串;
- str(e):返回异常信息,但不包括异常信息的类型;
- repr(e):返回较全的异常信息,包括异常信息的类型。
举个例子:
xxxxxxxxxx121try:2 1/03except Exception as e:4 # 访问异常的错误编号和详细信息5 print(e.args)6 print(str(e))7 print(repr(e))8 9# 输出结果为:10# > ('division by zero',)11# > division by zero12# > ZeroDivisionError('division by zero',)从程序中可以看到,由于 except 可能接收多种异常,因此为了操作方便,可以直接给每一个进入到此 except 块的异常,起一个统一的别名 e。
4. try except else
在原本的try except结构的基础上,Python 异常处理机制还提供了一个 else 块,也就是原有 try except 语句的基础上再添加一个 else 块,即try except else结构。
使用 else 包裹的代码,只有当 try 块没有捕获到任何异常时,才会得到执行;反之,如果 try 块捕获到异常,即便调用对应的 except 处理完异常,else 块中的代码也不会得到执行。
xxxxxxxxxx161try:2 result = 20 / int(input('请输入除数:'))3 print(result)4except ValueError:5 print('必须输入整数')6except ArithmeticError:7 print('算术错误,除数不能为 0')8else:9 print('没有出现异常')10print("继续执行")11
12# 执行结果13# 请输入除数:414# 5.015# 没有出现异常16# 继续执行将 else 包裹的代码 放置 try except 后面与放在 else 中的区别是:else 中的代码,只有当 try 块没有捕捉到异常的时候才会执行。如果放置在 try except 后面,即使捕捉到了异常,后续程序都会被依次执行。
5. 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 块是否发生异常,该块中的代码都会被执行。
xxxxxxxxxx151try:2 a = int(input("请输入 a 的值:"))3 print(20/a)4except:5 print("发生异常!")6else:7 print("执行 else 块中的代码") 8finally :9 print("执行 finally 块中的代码")10 11# 运行结果12# 请输入 a 的值:413# 5.014# 执行 else 块中的代码15# 执行 finally 块中的代码
6. raise 用法
raise 的作用就是可以让我们在程序的指定位置手动抛出一个异常。
为什么还要手动设置异常?
首先要分清楚程序发生异常和程序执行错误,它们完全是两码事,程序由于错误导致的运行异常,是需要程序员想办法解决的;但还有一些异常,是程序正常运行的结果,比如用 raise 手动引发的异常。
raise 语句的基本语法格式为:
xxxxxxxxxx11raise [exceptionName [(reason)]]其中,用 [] 括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则 raise 会把当前错误原样抛出;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。
也就是说,raise 语句有如下三种常用的用法:
- raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。
- raise 异常类名称:raise 后带一个异常类名称,表示引发执行类型的异常。
- raise 异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。
xxxxxxxxxx171>>> raise2Traceback (most recent call last):3 File "<pyshell#1>", line 1, in <module>4 raise5RuntimeError: No active exception to reraise6
7>>> raise ZeroDivisionError8Traceback (most recent call last):9 File "<pyshell#0>", line 1, in <module>10 raise ZeroDivisionError11ZeroDivisionError12
13>>> raise ZeroDivisionError("除数不能为零")14Traceback (most recent call last):15 File "<pyshell#2>", line 1, in <module>16 raise ZeroDivisionError("除数不能为零")17ZeroDivisionError: 除数不能为零当然,手动让程序引发异常,很多时候并不是为了让其崩溃。事实上,raise 语句引发的异常通常用 try except(else finally)异常处理结构来捕获并进行处理。例如:
xxxxxxxxxx121try:2 a = input("输入一个数:")3 #判断用户输入的是否为数字4 if(not a.isdigit()):5 raise ValueError("a 必须是数字")6except ValueError as e:7 print("引发异常:",repr(e))8
9
10# 程序运行结果为:11# > 输入一个数:a12# > 引发异常: ValueError('a 必须是数字',)可以看到,当用户输入的不是数字时,程序会进入 if 判断语句,并执行 raise 引发 ValueError 异常。但由于其位于 try 块中,因为 raise 抛出的异常会被 try 捕获,并由 except 块进行处理。
因此,虽然程序中使用了 raise 语句引发异常,但程序的执行是正常的,手动抛出的异常并不会导致程序崩溃。
当在没有引发过异常的程序使用无参的 raise 语句时,它默认引发的是 RuntimeError 异常。例如:
xxxxxxxxxx101try:2 a = input("输入一个数:")3 if(not a.isdigit()):4 raise5except RuntimeError as e:6 print("引发异常:",repr(e))7
8# 程序执行结果为:9> 输入一个数:a10> 引发异常: RuntimeError('No active exception to reraise',)7. 更加详细的异常信息 traceback
traceback模块可以打印出程序当前具体的异常信息。常用的函数是:
- traceback.format_exc(): 以字符串返回异常信息
- traceback.print_exc():直接打印出异常信息
xxxxxxxxxx161import time2import traceback3
4
5def error_func():6 raise ValueError("错误出现")7
8
9if __name__ == '__main__':10 try:11 error_func()12 except ValueError:13 print(traceback.format_exc())14
15 time.sleep(1)16 print('继续执行')traceback.print_exc()和traceback.format_exc()输出的错误信息是一模一样的。traceback.print_exc()可以填入file参数,把异常信息填入到指定的file里。
xxxxxxxxxx11traceback.print_exc(file=open('error.txt','a'))