Skip to content

异常捕获与处理 ​

看懂异常以后,还要判断这个问题应该直接修复,还是需要程序正常应对。哪些异常可以交给程序处理,处理后又应该给出什么响应?

用户输入了不能转换成数字的内容、第一次运行时数据文件还不存在,这些情况可以预期,程序可以给出提示或采用备用方案。变量名写错、属性名写错通常属于代码问题,应该找到原因并修复。

异常处理不是让错误消失,而是让程序在遇到可预期问题时作出明确响应。

异常处理边界 ​

写 try...except 前,先判断异常是否属于程序能够正常应对的问题:

  1. 这个问题在程序正常使用时是否可能出现?
  2. 出现以后,程序是否有清楚、合理的处理办法?

例如:

情况常见做法
用户输入的内容不能转成整数提示用户重新输入
首次启动时数据文件不存在使用空数据启动,或创建初始文件
网络请求暂时失败提示稍后重试
变量名拼错导致 NameError修改代码
调用了对象不存在的方法检查对象和方法名

不要看到异常就立刻包一层 try...except。先读懂 Traceback,再决定处理位置和处理方式。

异常处理决策树:代码自身缺陷修复 vs 运行环境容错应对

try...except 基础结构 ​

先看一个最常见的需求:把用户输入转成整数。

python
try:
    number = int(input("请输入一个整数:"))
except ValueError as error:
    print("输入内容不是有效的整数")
    print(error)

print("程序执行完毕")

执行过程如下:

  1. Python 先执行 try 中的代码。
  2. 输入可以转换成整数时,except 不会执行。
  3. 输入 abc 时,int() 抛出 ValueError。
  4. Python 跳到匹配的 except,输出提示。
  5. 异常已经被处理,程序继续执行后面的代码。

as error 会把这一次的异常对象保存到变量中。需要查看 Python 提供的具体错误说明时,可以输出它;不需要使用异常对象时,也可以省略这一部分。

try、except、else、finally 的执行分支

缩小 try 的范围

try 中只放确实可能抛出目标异常的代码。范围越清楚,越容易判断究竟是哪一步出了问题。

多异常分支 ​

同一段代码可能遇到不同问题。处理方式不同时,分别编写 except;处理方式相同时,可以把异常类型写成元组。

分离异常分支 ​

float() 接收普通字符串时可能抛出 ValueError,接收字典等不支持的对象时可能抛出 TypeError:

python
def convert_to_float(data):
    try:
        return float(data)
    except ValueError:
        print("当前内容不能转换成浮点数")
        return None
    except TypeError:
        print("请传入数字或字符串")
        return None

测试三种输入:

python
>>> convert_to_float("3.14")
3.14
>>> convert_to_float("abc")
当前内容不能转换成浮点数
>>> convert_to_float({"name": "正心"})
请传入数字或字符串

不同异常进入不同分支,因此可以给出更准确的提示。

异常类型合并 ​

如果几种异常的处理动作完全一样,可以写成一个元组:

python
def convert_to_float(data):
    try:
        return float(data)
    except (ValueError, TypeError):
        print("参数必须是数字或数字字符串")
        return None

这里的括号不能省略:

python
except (ValueError, TypeError):

兜底异常处理 ​

Exception 是很多常见异常的父类,可以作为最后的兜底分支:

python
try:
    number = int(input("请输入一个整数:"))
except ValueError:
    print("输入内容不是有效的整数")
except Exception as error:
    print(f"出现了暂时无法处理的问题:{error}")

具体异常要写在前面,范围更大的 Exception 放在最后。平时不要直接这样写:

python
try:
    number = int(input("请输入一个整数:"))
except Exception:
    pass

这段代码会把错误安静地吞掉,既没有处理结果,也没有留下排查线索。

except 漏斗匹配顺序机制与静默吞错黑洞禁忌

成功分支与收尾动作 ​

except 负责异常分支,else 负责成功分支,finally 负责最后一定要执行的动作。

else 成功分支 ​

python
try:
    number = int(input("请输入一个整数:"))
except ValueError:
    print("输入内容不是有效的整数")
else:
    print(f"转换结果:{number}")

只有 try 没有抛出异常时,else 才会执行。把成功后的操作放进 else,可以继续保持 try 的范围清楚。

finally 收尾分支 ​

python
file = None

try:
    file = open("data.txt", encoding="utf-8")
    print(file.read())
except FileNotFoundError:
    print("文件不存在")
finally:
    if file is not None:
        file.close()
    print("本次读取结束")

文件存在时会正常读取并关闭;文件不存在时会进入 except。无论结果如何,finally 都会执行。

前面已经学过的 with open(...) 更适合普通文件操作,因为它会自动关闭文件。finally 还可以用于其他必须执行的收尾动作,例如关闭连接、释放资源或恢复临时状态。

try 语句完整结构 ​

try 语句可以由下面几部分组成:

python
try:
    # 可能抛出异常的代码
    pass
except ValueError as error:
    # 处理指定异常
    pass
except (TypeError, ZeroDivisionError) as error:
    # 多种异常使用相同处理方式
    pass
except Exception as error:
    # 最后的兜底处理
    pass
else:
    # 没有异常时执行
    pass
finally:
    # 无论结果如何都执行
    pass

实际代码不需要每次写全。根据需求选择 except、else 和 finally。

异常传递与处理边界 ​

函数内部没有处理的异常,会沿调用过程继续向外传递:

python
def parse_score(text):
    return float(text)


def build_message(text):
    score = parse_score(text)
    return f"成绩:{score}"


def main():
    print(build_message("abc"))


main()

ValueError 从 parse_score() 依次传到 build_message() 和 main(),最后因为没有被处理而结束程序。上一章看到的多层 Traceback,记录的正是这条传递路径。

函数内部处理 ​

如果函数的职责就是完成一次安全转换,可以在内部处理:

python
def convert_to_float(data):
    try:
        return float(data)
    except (ValueError, TypeError):
        return None

调用者需要知道 None 表示转换失败,不能继续把它当成浮点数使用。

外层统一处理 ​

如果外层更清楚应该怎样提示用户,可以让异常继续传递:

python
def parse_score(text):
    return float(text)


try:
    score = parse_score("abc")
except ValueError:
    print("成绩必须是数字")

处理位置没有固定答案。把异常交给最清楚后续动作的那一层处理。

异常的传递与处理边界

记录后重新抛出 ​

有时当前层只负责记录问题,真正的处理仍然交给外层。这时可以使用 raise 继续抛出当前异常:

python
import logging


try:
    number = int("abc")
except ValueError:
    logging.exception("数字转换失败")
    raise

logging.exception() 应该在异常处理代码中使用,它会记录错误说明和完整 Traceback。单独写一个 raise,会继续抛出当前正在处理的异常。

异常响应决策:优雅降级 (Fallback) vs 熔断重抛 (Re-raise)

综合案例:安全加载学生数据 ​

需求: 为学生管理系统补充数据加载逻辑,分别处理数据文件不存在和 JSON 内容损坏两种情况。

功能分析: 文件不存在时使用空列表启动;文件内容损坏时记录完整 Traceback 并终止当前流程;读取成功时输出加载数量并返回学生列表。

沿用文件操作章节中的学生管理系统,实现这个加载函数:

python
import json
import logging
from pathlib import Path


logging.basicConfig(
    level=logging.ERROR,
    format="%(levelname)s | %(message)s",
)


def load_students(file_path):
    try:
        with file_path.open(encoding="utf-8") as file:
            students = json.load(file)
    except FileNotFoundError:
        print("学生数据文件不存在,使用空列表启动")
        return []
    except json.JSONDecodeError:
        logging.exception("学生数据文件格式错误")
        raise
    else:
        print(f"成功加载 {len(students)} 条学生信息")
        return students


def main():
    data_path = Path(__file__).with_name("students.json")
    student_list = load_students(data_path)
    print(student_list)


if __name__ == "__main__":
    main()

要观察读取成功的分支,在 main.py 同级目录中新建 students.json:

json
[
  {
    "name": "张三",
    "age": 18,
    "mobile": "13800000000"
  },
  {
    "name": "李四",
    "age": 19,
    "mobile": "13900000000"
  }
]

要观察文件不存在的分支,暂时将这个文件改名;要观察格式错误的分支,再把文件内容改成不完整的 JSON。每次运行前先确认当前文件状态。

这里对两种异常采用不同策略:

  1. 文件不存在可能是第一次启动,因此返回空列表。
  2. 文件存在但内容损坏时,不能把它当成空数据继续运行,否则后续保存可能覆盖原数据。程序先记录完整 Traceback,再继续抛出异常。
  3. 文件正常读取时,else 输出加载结果并返回学生列表。

异常处理的目标不是让程序永远不停止,而是针对不同问题作出明确选择。

小结 ​

本节重点掌握六件事:

  1. 先判断异常是否属于程序能够正常应对的问题。
  2. try 中只放可能抛出目标异常的代码。
  3. 优先捕获具体异常,兜底的 Exception 放在最后。
  4. else 处理成功分支,finally 处理必须执行的收尾动作。
  5. 异常可以在当前函数处理,也可以传给更清楚处理方式的外层。
  6. 捕获异常后仍需继续传递时,可以记录信息并使用 raise。

练习 ​

  1. 把用户输入转换成整数,分别测试正常数字和非数字内容。
  2. 给转换程序补充 else 和 finally,观察四个代码块的执行顺序。
  3. 分别测试学生数据文件不存在、内容正确和 JSON 格式损坏三种情况。