跳转至

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的代码块

回到上面的例子,如果我们想同时捕获 ZeroDivisionErrorIndexError,可以这样写:

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的代码块

回到上面的例子,如果我们想让 ZeroDivisionErrorIndexError 使用同一段处理代码,可以这样写:

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
捕获到异常:您的赋值的不是整数!