1. 前言
在自己摸索开发的过程中,程序运行时难免会碰到一些错误,尤其当前后端交互时出现各种资源问题,例如404,500等,在写前端代码的时候,就深知异常处理机制是十分重要的,因此在学习python时,有必要去学习一下Python程序的异常处理机制。
当Python程序遇到一些错误,例如除数为 0、年龄为负数、数组下标越界等,这些错误如果不能发现并加以处理,很可能会导致程序崩溃。处理异常机制的存在,可以让我们捕获并处理这些错误,让程序继续沿着一条不会出错的路径执行。
可以简单的理解异常处理机制,就是在程序运行出现错误时,让 Python 解释器执行事先准备好的除错程序,进而尝试恢复程序的执行。
借助异常处理机制,甚至在程序崩溃前也可以做一些必要的工作,例如将内存中的数据写入文件、关闭打开的文件、释放分配的内存等。
Python 异常处理机制会涉及 try
、except
、else
、finally
这 4 个关键字,同时还提供了可主动使程序引发异常的 raise
语句。
2. 常见的异常类型
编写程序时遇到的错误大致分为两类:语法错误 和 运行时错误。
2.1 语法错误
语法错误,也就是解析代码时出现的错误。当代码不符合 Python 语法规则时,Python解释器在解析时就会报出 SyntaxError 语法错误,与此同时还会明确指出最早探测到错误的语句。例如:
xxxxxxxxxx
11print "Hello,World!"
我们知道,Python 3 已不再支持上面这种写法,所以在运行时,解释器会报如下错误:
xxxxxxxxxx
11SyntaxError: Missing parentheses in call to 'print'
语法错误多是开发者疏忽导致的,属于真正意义上的错误,是解释器无法容忍的,因此,只有将程序中的所有语法错误全部纠正,程序才能执行。
2.2 运行时错误
运行时错误,即程序在语法上都是正确的,但在运行时发生了错误。例如:
xxxxxxxxxx
11a = 1/0
上面这句代码的意思是“用 1 除以 0,并赋值给 a 。因为 0 作除数是没有意义的,所以运行后会产生如下错误:
xxxxxxxxxx
51>>> a = 1/0
2Traceback (most recent call last):
3 File "<pyshell#2>", line 1, in <module>
4 a = 1/0
5ZeroDivisionError: 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
语句块捕获并处理异常,其基本语法结构如下所示:
xxxxxxxxxx
81try:
2 # 可能产生异常的代码块
3except [ (Error1, Error2, ... ) [as e] ]:
4 # 处理异常的代码块1
5except [ (Error3, Error4, ... ) [as e] ]:
6 # 处理异常的代码块2
7except [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 处理完成,则程序可以继续执行。
xxxxxxxxxx
151try:
2 a = int(input("输入被除数:"))
3 b = int(input("输入除数:"))
4 c = a / b
5 print("您输入的两个数相除的结果是:", c )
6except (ValueError, ArithmeticError):
7 print("程序发生了数字格式异常、算术异常之一")
8except :
9 print("未知异常")
10print("程序继续运行")
11
12# 程序运行结果为:
13# > 输入被除数:a
14# > 程序发生了数字格式异常、算术异常之一
15# > 程序继续运行
由于 try 块中引发了异常,并被 except 块成功捕获,因此程序才可以继续执行,才有了“程序继续运行”的输出结果。
3.2 获取特定异常的有关信息
由于一个 except 可以同时处理多个异常,那么就需要知道当前处理的到底是哪种异常,毕竟总不能每个异常都写一个 except。
每种异常类型都提供了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的相关信息:
- args:返回异常的错误编号和描述字符串;
- str(e):返回异常信息,但不包括异常信息的类型;
- repr(e):返回较全的异常信息,包括异常信息的类型。
举个例子:
xxxxxxxxxx
121try:
2 1/0
3except 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 zero
12# > 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 块中的代码也不会得到执行。
xxxxxxxxxx
161try:
2 result = 20 / int(input('请输入除数:'))
3 print(result)
4except ValueError:
5 print('必须输入整数')
6except ArithmeticError:
7 print('算术错误,除数不能为 0')
8else:
9 print('没有出现异常')
10print("继续执行")
11
12# 执行结果
13# 请输入除数:4
14# 5.0
15# 没有出现异常
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 块是否发生异常,该块中的代码都会被执行。
xxxxxxxxxx
151try:
2 a = int(input("请输入 a 的值:"))
3 print(20/a)
4except:
5 print("发生异常!")
6else:
7 print("执行 else 块中的代码")
8finally :
9 print("执行 finally 块中的代码")
10
11# 运行结果
12# 请输入 a 的值:4
13# 5.0
14# 执行 else 块中的代码
15# 执行 finally 块中的代码
6. raise 用法
raise 的作用就是可以让我们在程序的指定位置手动抛出一个异常。
为什么还要手动设置异常?
首先要分清楚程序发生异常和程序执行错误,它们完全是两码事,程序由于错误导致的运行异常,是需要程序员想办法解决的;但还有一些异常,是程序正常运行的结果,比如用 raise 手动引发的异常。
raise 语句的基本语法格式为:
xxxxxxxxxx
11raise [exceptionName [(reason)]]
其中,用 []
括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则 raise 会把当前错误原样抛出;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。
也就是说,raise 语句有如下三种常用的用法:
- raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。
- raise 异常类名称:raise 后带一个异常类名称,表示引发执行类型的异常。
- raise 异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。
xxxxxxxxxx
171>>> raise
2Traceback (most recent call last):
3 File "<pyshell#1>", line 1, in <module>
4 raise
5RuntimeError: No active exception to reraise
6
7>>> raise ZeroDivisionError
8Traceback (most recent call last):
9 File "<pyshell#0>", line 1, in <module>
10 raise ZeroDivisionError
11ZeroDivisionError
12
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)
异常处理结构来捕获并进行处理。例如:
xxxxxxxxxx
121try:
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# > 输入一个数:a
12# > 引发异常: ValueError('a 必须是数字',)
可以看到,当用户输入的不是数字时,程序会进入 if 判断语句,并执行 raise 引发 ValueError 异常。但由于其位于 try 块中,因为 raise 抛出的异常会被 try 捕获,并由 except 块进行处理。
因此,虽然程序中使用了 raise 语句引发异常,但程序的执行是正常的,手动抛出的异常并不会导致程序崩溃。
当在没有引发过异常的程序使用无参的 raise 语句时,它默认引发的是 RuntimeError 异常。例如:
xxxxxxxxxx
101try:
2 a = input("输入一个数:")
3 if(not a.isdigit()):
4 raise
5except RuntimeError as e:
6 print("引发异常:",repr(e))
7
8# 程序执行结果为:
9> 输入一个数:a
10> 引发异常: RuntimeError('No active exception to reraise',)
7. 更加详细的异常信息 traceback
traceback模块可以打印出程序当前具体的异常信息。常用的函数是:
- traceback.format_exc(): 以字符串返回异常信息
- traceback.print_exc():直接打印出异常信息
xxxxxxxxxx
161import time
2import traceback
3
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里。
xxxxxxxxxx
11traceback.print_exc(file=open('error.txt','a'))