Topic 2.9 - 异常处理¶
Python 运行异常大家也见过一些了,对于异常的处理我们有两个重要的方面:
- 捕获异常:有错变没错:当程序出现异常时,我们可以通过捕获异常来避免程序崩溃,并且可以根据异常类型进行相应的处理
- 抛出异常:没错变有错:当我们在编写函数时,如果遇到一些不合法的输入或者无法处理的情况,我们可以主动抛出异常来提醒调用者
1. 异常的概念¶
异常是程序在运行过程中出现的错误情况,就是大家俗称的“bug”。当程序出现异常时,程序会被迫中止运行,无法继续执行后续代码。
其实我们目前为止已经见到过很多异常了,比如:
| 异常名称 | 解释说明 | 示例代码 |
|---|---|---|
| NameError | 使用了未定义的变量。 | print(x) # x还没有被定义就使用 |
| ValueError | 当传入函数的参数类型正确,但值不合适时发生。 | int("abc") # 无法将字符串转换为整数 |
| TypeError | 操作或函数应用于不合适的类型。 | "a" + 1 # 字符串与整数不能相加 |
| ZeroDivisionError | 除法或取模操作的除数为 0。 | 10 / 0 # 除以零错误 |
| IndexError | 访问列表、元组或字符串时下标越界。 | print([1,2,3][3]) |
我们目前为止写的代码中如果有异常,程序会直接终止运行
- 但是大家在生活中使用的手机电脑APP也会遇到bug,但是它们并不会直接崩溃退出
- 有的程序会以一种错误奇怪的方式继续运行,比方说游戏里角色突然卡到墙里了,卡一会可能自己又跑出来了,然后游戏继续运行
- 有的程序则会弹出一个错误提示框,告诉你哪里出错了,然后提醒你回到上一步操作
- 那这些程序是怎么做到遇到bug不退出的呢?它们是通过捕获异常来实现的
除此之外,我们在编写程序时,还可能会遇到一些不合适的情况:
- 这些情况并不会导致程序崩溃,但是我们觉得遇到这些不合适的情况,程序就不应该再继续往下执行了
- 比如说我们写了一个函数,这个函数要求输入一个正整数,如果用户输入了一个负数或者零,我们觉得这个输入就是不合法的,就不希望程序继续往下执行了
- 这时候我们就可以通过抛出异常来提醒调用者这个输入不合法了,程序不应该继续往下执行了
2. 捕获异常¶
(1) 异常捕获的基本语法¶
Python 提供了异常处理机制,允许我们在程序中捕获和处理异常,从而避免程序崩溃。
try:
可能会引发异常的代码块
except:
处理异常类型的代码块
这段代码中:
try中的代码是我们认为可能会引发异常的代码。如果这些代码运行时没有发生异常,程序会继续执行try块后面的代码- 如果
try块中的代码引发了异常,程序会跳转到except块,执行其中的代码来处理异常 - 如果
try块中的代码没有引发异常,except块中的代码将不会被执行
我们来看一个简单的例子:
try:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(len(list_a)):
print(10 / list_a[i])
except:
print("程序出现异常,请好好检查一下代码")
10.0
5.0
3.3333333333333335
2.5
2.0
程序出现异常,请好好检查一下代码
在这个例子中,程序在执行到 10 / list_a[5] 时遇到了除零错误
- 但是程序并没有崩溃,而是跳转到了
except块,输出了错误提示信息 - 最终程序是正常结束的
(2) 捕获特定类型异常¶
异常处理的基本语法结构如下:
try:
可能会引发异常的代码块
except 异常类型:
处理异常类型的代码块
这段代码中:
try中的代码是我们认为可能会引发异常的代码。如果这些代码运行时没有发生异常,程序会继续执行try块后面的代码- 如果
try块中的代码引发了指定类型的异常,程序会跳转到对应的except块,执行其中的代码来处理异常 - 如果
try块中的代码引发了其他类型的异常,程序仍然会崩溃
例如,我们可以使用异常处理来捕获除零错误:
try:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(len(list_a)):
print(10 / list_a[i])
except ZeroDivisionError:
print("除以零错误,无法进行除法运算")
10.0
5.0
3.3333333333333335
2.5
2.0
除以零错误,无法进行除法运算
可以看到,虽然遇到了除零错误,程序并没有崩溃,而是执行了 except 块中的代码,输出了错误提示信息,最终程序是正常结束的。
我们再来看以下例子,在这个例子中,程序遇到了除了除零错误以外的其他类型的异常,还是会报错:
# 以下代码还是会报错 IndexError,请取消注释后尝试运行
try:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7): # 注意这里,循环次数超过了列表长度
print(list_a[i])
except ZeroDivisionError:
print("除以零错误,无法进行除法运算")
1
2
3
4
5
0
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[17], line 5
3 list_a = [1, 2, 3, 4, 5, 0]
4 for i in range(7): # 注意这里,循环次数超过了列表长度
----> 5 print(list_a[i])
6 except ZeroDivisionError:
7 print("除以零错误,无法进行除法运算")
IndexError: list index out of range
这里,程序在遇到 IndexError 时还是崩溃了,因为我们只捕获 ZeroDivisionError 类型的异常,没有捕获 IndexError。
(3) 捕获异常后程序便不再运行 try 块中的后续代码¶
注意,在上个例子中,我为什么把 print(10 / list_a[i]) 改成了 print(list_a[i]) 呢,我们不妨试一试还用 print(10 / list_a[i]) 会发生什么:
try:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7): # 注意这里,循环次数超过了列表长度
print(10 / list_a[i])
except ZeroDivisionError:
print("除以零错误,无法进行除法运算")
10.0
5.0
3.3333333333333335
2.5
2.0
除以零错误,无法进行除法运算
可以看到,我们期待的是程序遇到 IndexError 时崩溃,但是程序实际上在遇到 ZeroDivisionError 时就已经跳转到了 except 块,输出了错误提示信息,程序并没有崩溃,具体来说:
- 当循环运行到
10 / list_a[5]时,引发了ZeroDivisionError,程序就已经跳转到了except块 - 因为程序已经跳转到了
except块,所以后续的循环(包括引发IndexError的那次10 / list_a[6]循环)都没有机会执行了
那么如果我们想让程序继续执行后续的循环,而不是在遇到第一个异常时就跳转到 except 块,该怎么办呢?我们可以把 try 块放到循环内部:
# 以下代码会报 IndexError,请取消注释后尝试运行
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7): # 注意这里,循环次数超过了列表长度
try:
print(10 / list_a[i])
except ZeroDivisionError:
print("除以零错误,无法进行除法运算")
10.0
5.0
3.3333333333333335
2.5
2.0
除以零错误,无法进行除法运算
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[19], line 5
3 for i in range(7): # 注意这里,循环次数超过了列表长度
4 try:
----> 5 print(10 / list_a[i])
6 except ZeroDivisionError:
7 print("除以零错误,无法进行除法运算")
IndexError: list index out of range
(4) 捕获多种异常¶
针对上面例子里反映出来的情况,我们可以添加多个 except 块来捕获不同类型的异常,基本语法是:
try:
可能会引发异常的代码块
except 异常类型1:
处理异常类型1的代码块
except 异常类型2:
处理异常类型2的代码块
回到上面的例子,如果我们想同时捕获 ZeroDivisionError 和 IndexError,可以这样写:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7):
try:
print(10 / list_a[i])
except ZeroDivisionError:
print("除以零错误,无法进行除法运算")
except IndexError:
print("索引错误,访问了不存在的列表元素")
10.0
5.0
3.3333333333333335
2.5
2.0
除以零错误,无法进行除法运算
索引错误,访问了不存在的列表元素
可以看到,程序分别捕获了两种不同类型的异常,并执行了对应的处理代码,最终程序是正常结束的:
- 当程序运行到
10 / list_a[5]时,捕获了ZeroDivisionError,执行了第一个except块 - 当程序运行到
10 / list_a[6]时,捕获了IndexError,执行了第二个except块
如果我们想让多个异常类型使用同一段处理代码,可以将它们放在一个括号内:
try:
可能会引发异常的代码块
except (异常类型1, 异常类型2):
处理异常类型1和类型2的代码块
回到上面的例子,如果我们想让 ZeroDivisionError 和 IndexError 使用同一段处理代码,可以这样写:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7):
try:
print(10 / list_a[i])
except (ZeroDivisionError, IndexError):
print("程序出现除零错误或索引错误,请好好检查一下代码")
10.0
5.0
3.3333333333333335
2.5
2.0
程序出现除零错误或索引错误,请好好检查一下代码
程序出现除零错误或索引错误,请好好检查一下代码
在这个例子中,无论是 ZeroDivisionError 还是 IndexError,程序都会执行同一段处理代码,输出相同的错误提示信息。
(5) 捕获异常的完整代码¶
异常处理的完整代码结构如下:
try:
可能会引发异常的代码块
except 异常类型1:
处理异常类型1的代码块
except 异常类型2:
处理异常类型2的代码块
except (异常类型3, 异常类型4):
处理异常类型3和类型4的代码块
else:
如果没有发生任何异常,执行的代码块
finally:
无论是否发生异常,最终都会执行的代码块
这段代码中:
else块中的代码在try块中的代码没有发生任何异常时执行finally块中的代码无论是否发生异常,都会被执行
我们来看一个完整的例子:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7):
try:
print(f"当前循环是第 {i} 次")
print(10 / list_a[i])
if i == 2:
print(x) # 故意引发 NameError
if i == 3:
print("a" + 1) # 故意引发 TypeError
except ZeroDivisionError:
print("除以零错误,无法进行除法运算")
except IndexError:
print("索引错误,访问了不存在的列表元素")
except (NameError, TypeError):
print("发生了名称错误或类型错误,请好好检查一下代码")
else:
print("本次循环没有发生任何异常")
finally:
print("本次循环结束")
print()
当前循环是第 0 次
10.0
本次循环没有发生任何异常
本次循环结束
当前循环是第 1 次
5.0
本次循环没有发生任何异常
本次循环结束
当前循环是第 2 次
3.3333333333333335
123.456
本次循环没有发生任何异常
本次循环结束
当前循环是第 3 次
2.5
发生了名称错误或类型错误,请好好检查一下代码
本次循环结束
当前循环是第 4 次
2.0
本次循环没有发生任何异常
本次循环结束
当前循环是第 5 次
除以零错误,无法进行除法运算
本次循环结束
当前循环是第 6 次
索引错误,访问了不存在的列表元素
本次循环结束
在这段代码的 for 循环中:
- 第 1 圈: 当前索引值是 0,执行
10 / list_a[0]没有异常,执行了else块,然后执行了finally块 - 第 2 圈: 当前索引值是 1,执行
10 / list_a[1]没有异常,执行了else块,然后执行了finally块 - 第 3 圈: 当前索引值是 2,执行
10 / list_a[2]没有异常,但是执行print(x)引发了NameError,跳转到对应的except块,然后执行了finally块 - 第 4 圈: 当前索引值是 3,执行
10 / list_a[3]没有异常,但是执行"a" + 1引发了TypeError,跳转到对应的except块,然后执行了finally块 - 第 5 圈: 当前索引值是 4,执行
10 / list_a[4]没有异常,执行了else块,然后执行了finally块 - 第 6 圈: 当前索引值是 5,执行
10 / list_a[5]引发了ZeroDivisionError,跳转到对应的except块,然后执行了finally块 - 第 7 圈: 当前索引值是 6,执行
10 / list_a[6]引发了IndexError,跳转到对应的except块,然后执行了finally块
(6) 保留异常信息¶
有时候我们在捕获异常后,想要知道具体的异常信息,可以使用 as 关键字将异常对象赋值给一个变量:
try:
可能会引发异常的代码块
except 异常类型 as e:
print(f"捕获到异常:{e}")
在这个例子中,我们将异常的信息,以字符串的形式,赋值给了 e 变量,e 将包含异常的具体信息,我们可以打印出来查看
我们来看一个例子:
list_a = [1, 2, 3, 4, 5, 0]
for i in range(7):
try:
print(10 / list_a[i])
except ZeroDivisionError as e:
print(f"捕获到异常:{e}")
except IndexError as e:
print(f"捕获到异常:{e}")
10.0
5.0
3.3333333333333335
2.5
2.0
捕获到异常:division by zero
捕获到异常:list index out of range
这里我们可以给两种不同的错误信息,都赋值给一个叫 e 的变量,这两个 e 变量其实是不会冲突的,因为它们分别在不同的 except 块中
3. 抛出异常¶
(1) 抛出异常的基本语法¶
到目前为止,我们见到的异常都是 Python 解释器自动抛出的:
- 这些异常都比较基础
- 如果出现这些异常,意味着程序的基本运行逻辑是不满足的。
在实际开发中,我们还可以“自定义”异常:
- 这些异常通常不是程序运行的基础逻辑错误,而是业务逻辑上的错误
- 这就要用到抛出异常的机制
抛出异常的基本语法如下:
raise 异常类型("异常的描述信息")
但是,通常,我们不会在程序主流程中平白无故就抛出异常,我们通常是程序在满足某个条件时,才抛出异常:
if 某个条件满足:
raise 异常类型("异常的描述信息")
我们来看以下例子:
# 请给x赋值一个整数
x = 123.456
print(x)
if not isinstance(x, int):
raise ValueError("您的输入的赋值的不是整数!")
123.456
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[24], line 5
3 print(x)
4 if not isinstance(x, int):
----> 5 raise ValueError("您的输入的赋值的不是整数!")
ValueError: 您的输入的赋值的不是整数!
(2) 捕获自定义的异常¶
通常我们会在 try 块中,判断如果某个条件满足,就使用 raise 抛出异常,并且我们还可以在 except 块中捕获这个异常:
try:
# 请给x赋值一个整数
x = 123.456
print(x)
if not isinstance(x, int):
raise ValueError("您的赋值的不是整数!")
except ValueError as e:
print(f"捕获到异常:{e}")
123.456
捕获到异常:您的赋值的不是整数!