异常捕获与处理
看懂异常以后,还要判断这个问题应该直接修复,还是需要程序正常应对。哪些异常可以交给程序处理,处理后又应该给出什么响应?
用户输入了不能转换成数字的内容、第一次运行时数据文件还不存在,这些情况可以预期,程序可以给出提示或采用备用方案。变量名写错、属性名写错通常属于代码问题,应该找到原因并修复。
异常处理不是让错误消失,而是让程序在遇到可预期问题时作出明确响应。
异常处理边界
写 try...except 前,先判断异常是否属于程序能够正常应对的问题:
- 这个问题在程序正常使用时是否可能出现?
- 出现以后,程序是否有清楚、合理的处理办法?
例如:
| 情况 | 常见做法 |
|---|---|
| 用户输入的内容不能转成整数 | 提示用户重新输入 |
| 首次启动时数据文件不存在 | 使用空数据启动,或创建初始文件 |
| 网络请求暂时失败 | 提示稍后重试 |
变量名拼错导致 NameError | 修改代码 |
| 调用了对象不存在的方法 | 检查对象和方法名 |
不要看到异常就立刻包一层 try...except。先读懂 Traceback,再决定处理位置和处理方式。
try...except 基础结构
先看一个最常见的需求:把用户输入转成整数。
try:
number = int(input("请输入一个整数:"))
except ValueError as error:
print("输入内容不是有效的整数")
print(error)
print("程序执行完毕")执行过程如下:
- Python 先执行
try中的代码。 - 输入可以转换成整数时,
except不会执行。 - 输入
abc时,int()抛出ValueError。 - Python 跳到匹配的
except,输出提示。 - 异常已经被处理,程序继续执行后面的代码。
as error 会把这一次的异常对象保存到变量中。需要查看 Python 提供的具体错误说明时,可以输出它;不需要使用异常对象时,也可以省略这一部分。
缩小 try 的范围
try 中只放确实可能抛出目标异常的代码。范围越清楚,越容易判断究竟是哪一步出了问题。
多异常分支
同一段代码可能遇到不同问题。处理方式不同时,分别编写 except;处理方式相同时,可以把异常类型写成元组。
分离异常分支
float() 接收普通字符串时可能抛出 ValueError,接收字典等不支持的对象时可能抛出 TypeError:
def convert_to_float(data):
try:
return float(data)
except ValueError:
print("当前内容不能转换成浮点数")
return None
except TypeError:
print("请传入数字或字符串")
return None测试三种输入:
>>> convert_to_float("3.14")
3.14
>>> convert_to_float("abc")
当前内容不能转换成浮点数
>>> convert_to_float({"name": "正心"})
请传入数字或字符串不同异常进入不同分支,因此可以给出更准确的提示。
异常类型合并
如果几种异常的处理动作完全一样,可以写成一个元组:
def convert_to_float(data):
try:
return float(data)
except (ValueError, TypeError):
print("参数必须是数字或数字字符串")
return None这里的括号不能省略:
except (ValueError, TypeError):兜底异常处理
Exception 是很多常见异常的父类,可以作为最后的兜底分支:
try:
number = int(input("请输入一个整数:"))
except ValueError:
print("输入内容不是有效的整数")
except Exception as error:
print(f"出现了暂时无法处理的问题:{error}")具体异常要写在前面,范围更大的 Exception 放在最后。平时不要直接这样写:
try:
number = int(input("请输入一个整数:"))
except Exception:
pass这段代码会把错误安静地吞掉,既没有处理结果,也没有留下排查线索。
成功分支与收尾动作
except 负责异常分支,else 负责成功分支,finally 负责最后一定要执行的动作。
else 成功分支
try:
number = int(input("请输入一个整数:"))
except ValueError:
print("输入内容不是有效的整数")
else:
print(f"转换结果:{number}")只有 try 没有抛出异常时,else 才会执行。把成功后的操作放进 else,可以继续保持 try 的范围清楚。
finally 收尾分支
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 语句可以由下面几部分组成:
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。
异常传递与处理边界
函数内部没有处理的异常,会沿调用过程继续向外传递:
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,记录的正是这条传递路径。
函数内部处理
如果函数的职责就是完成一次安全转换,可以在内部处理:
def convert_to_float(data):
try:
return float(data)
except (ValueError, TypeError):
return None调用者需要知道 None 表示转换失败,不能继续把它当成浮点数使用。
外层统一处理
如果外层更清楚应该怎样提示用户,可以让异常继续传递:
def parse_score(text):
return float(text)
try:
score = parse_score("abc")
except ValueError:
print("成绩必须是数字")处理位置没有固定答案。把异常交给最清楚后续动作的那一层处理。
记录后重新抛出
有时当前层只负责记录问题,真正的处理仍然交给外层。这时可以使用 raise 继续抛出当前异常:
import logging
try:
number = int("abc")
except ValueError:
logging.exception("数字转换失败")
raiselogging.exception() 应该在异常处理代码中使用,它会记录错误说明和完整 Traceback。单独写一个 raise,会继续抛出当前正在处理的异常。
综合案例:安全加载学生数据
需求: 为学生管理系统补充数据加载逻辑,分别处理数据文件不存在和 JSON 内容损坏两种情况。
功能分析: 文件不存在时使用空列表启动;文件内容损坏时记录完整 Traceback 并终止当前流程;读取成功时输出加载数量并返回学生列表。
沿用文件操作章节中的学生管理系统,实现这个加载函数:
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:
[
{
"name": "张三",
"age": 18,
"mobile": "13800000000"
},
{
"name": "李四",
"age": 19,
"mobile": "13900000000"
}
]要观察文件不存在的分支,暂时将这个文件改名;要观察格式错误的分支,再把文件内容改成不完整的 JSON。每次运行前先确认当前文件状态。
这里对两种异常采用不同策略:
- 文件不存在可能是第一次启动,因此返回空列表。
- 文件存在但内容损坏时,不能把它当成空数据继续运行,否则后续保存可能覆盖原数据。程序先记录完整 Traceback,再继续抛出异常。
- 文件正常读取时,
else输出加载结果并返回学生列表。
异常处理的目标不是让程序永远不停止,而是针对不同问题作出明确选择。
小结
本节重点掌握六件事:
- 先判断异常是否属于程序能够正常应对的问题。
try中只放可能抛出目标异常的代码。- 优先捕获具体异常,兜底的
Exception放在最后。 else处理成功分支,finally处理必须执行的收尾动作。- 异常可以在当前函数处理,也可以传给更清楚处理方式的外层。
- 捕获异常后仍需继续传递时,可以记录信息并使用
raise。
练习
- 把用户输入转换成整数,分别测试正常数字和非数字内容。
- 给转换程序补充
else和finally,观察四个代码块的执行顺序。 - 分别测试学生数据文件不存在、内容正确和 JSON 格式损坏三种情况。