16  异常处理

写代码时,出错是常有的事。

有些错误是语法错误——代码还没跑起来,Python 就告诉你哪里写错了,比如漏了冒号、括号不匹配。这类错误很好修,看一眼就知道。

更多的错误发生在运行时:用户输入了意料之外的数、要读的文件不存在、列表索引越界……这些错误在 Python 中统称为异常(exception)。

本章学习如何让程序在遇到异常时不直接崩溃,而是优雅地处理。

16.1 常见异常

先来看几种你很可能已经见过的异常:

# 除以零
1 / 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[1], line 2
      1 # 除以零
----> 2 1 / 0

ZeroDivisionError: division by zero
# 类型正确,但值不合适
int('abc')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 2
      1 # 类型正确,但值不合适
----> 2 int('abc')

ValueError: invalid literal for int() with base 10: 'abc'
# 列表索引越界
[1, 2, 3][10]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[3], line 2
      1 # 列表索引越界
----> 2 [1, 2, 3][10]

IndexError: list index out of range
# 字典键不存在
{'a': 1}['b']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[4], line 2
      1 # 字典键不存在
----> 2 {'a': 1}['b']

KeyError: 'b'
# 使用未定义的变量
print(x)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 2
      1 # 使用未定义的变量
----> 2 print(x)

NameError: name 'x' is not defined

每种异常都有自己的名字——ZeroDivisionErrorValueErrorIndexError 等。看到报错时,最后一行的开头就是异常名。

下面是编写 Python 程序时最常见的异常:

异常名 含义 典型触发场景
ValueError 操作或函数收到类型正确但值不合适的参数 int("abc")、转换输入数据
ZeroDivisionError 除数为零 1 / 0
IndexError 序列索引超出范围 [1,2,3][10]
KeyError 字典键不存在 {"a": 1}["b"]
FileNotFoundError 文件不存在 open('不存在的文件.txt')
TypeError 对不支持该操作的类型进行操作 'hello' + 5
NameError 使用了未定义的变量 print(x) 但 x 未定义
注记异常也是类

上一章学过:Python 中一切都是对象。异常也不例外——ValueErrorZeroDivisionError 等都是类,它们抛出的异常实例就是这些类的实例。

所有内置异常都继承自 Exception 类。因此编写 except Exception: 可以捕获几乎所有的常见异常。

16.2 捕获异常:try-except

如果一段代码可能抛出异常,可以用 try-except 捕获它,而不是让程序直接崩溃。

try:
    # 可能出错的代码
except 异常类型:
    # 出错时执行的代码

try 块中的代码抛出了指定类型的异常时,程序会跳过 try 块中剩余的代码,直接进入对应的 except 块执行。如果 try 块没有抛出异常,except 块被跳过。

# 没有异常处理的版本:输入 'abc' 直接崩溃
x = int(input('请输入一个数字:'))
# 有异常处理的版本:输入 'abc' 也不会崩溃
try:
    x = int(input('请输入一个数字:'))
except ValueError:
    print('输入无效,请确保输入的是数字')

同一个 try 后面可以跟多个 except,分别处理不同类型的异常:

try:
    data = input('请输入除数:')
    num = int(data)
    result = 100 / num
except ValueError:
    print('输入的不是有效数字')
except ZeroDivisionError:
    print('除数不能为零')

如果多个异常的处理方式相同,也可以写在一起:

try:
    data = input('请输入除数:')
    num = int(data)
    result = 100 / num
except (ValueError, ZeroDivisionError):
    print('输入有误,请检查')

有时不仅需要捕获异常,还想拿到异常对象本身查看详细信息。使用 as 关键字:

try:
    1 / 0
except ZeroDivisionError as e:
    print(f'发生了异常:{e}')
发生了异常:division by zero

e 就是捕获到的异常实例,打印它可以看到异常的具体描述。

16.3 else 和 finally

try-except 还有两个可选块:elsefinally

else:try 块没有抛出异常时执行。 用于放置正常情况下要继续的代码,避免把它们塞在 try 块里被误捕获。

try:
    data = int(input('请输入数字:'))
except ValueError:
    print('输入无效')
else:
    # 只有没发生异常时才执行
    print(f'有效输入:{data}')

finally:无论是否发生异常都会执行。 适合放置”收尾”代码,比如关闭文件、释放资源等。

try:
    data = int(input('请输入数字:'))
except ValueError:
    print('输入无效')
else:
    print(f'有效输入:{data}')
finally:
    print('程序执行完毕')

即使 try 或 except 中包含 return 语句,finally 也会在函数真正返回之前执行。这确保了资源一定能被释放,是文件操作等场景中的标准模式。

# 具有try, except, else, finally的代码的执行逻辑
# 伪代码
执行'try'语句块
if 执行'try'语句块时出现异常:
    执行'except'
else:
    执行'else'
执行'finally'

16.4 手动抛出异常:raise

try-except接收异常。反过来,你也可以用 raise 关键字主动抛出异常。

什么时候需要自己抛异常?看一个例子:

def withdraw(balance, amount):
    if amount > balance:
        return -1          # 用特殊返回值表示错误
    return balance - amount

# 调用者很容易忘记检查返回值
result = withdraw(100, 200)
# 拿着 -1 继续算,结果越来越离谱

raise 改写:

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError('余额不足')
    return balance - amount

# 调用时如果不处理,程序会直接报错——迫使调用者处理
try:
    result = withdraw(100, 200)
except ValueError as e:
    print(e)
余额不足

raisereturn 的区别:

  • return:正常返回一个值,调用方继续执行
  • raise:中断当前函数,异常沿着调用栈向上传播。如果一路上都没有 try-except 捕获,程序就崩溃

raise 让函数更”诚实”——遇到处理不了的情况就直接报告,而不是返回一个容易被人忽略的特殊值。

常见的用法是在函数开头校验参数:

def set_score(score):
    if not 0 <= score <= 100:
        raise ValueError('成绩必须在 0 到 100 之间')
    print(f'设置成绩:{score}')

16.5 练习

  1. 以下代码会抛出什么类型的异常?请用 try-except 捕获它,使得程序能“跳过坏数据,继续处理好数据”。
data = ["10", "20", "三十", "40"]
total = 0
for item in data:
    total += int(item)   
  1. 编写一个除法函数 safe_divide(a, b),要求:
    • 如果 b 为 0,抛出 ZeroDivisionError(用 raise
    • 如果 ab 不是数字(intfloat),抛出 TypeError
    • 正常情况返回 a / b