Python 错误与调试图解:异常在哪里发生,又去了哪里
沿文件读取、JSON 解析和金额转换的运行路径,图解异常发生位置、except 匹配与向外传播,以及批量订单怎样记录坏数据并继续。
本文目录 7 节 · 点击展开
Python 报错时,真正需要回答的不是“怎么让红字消失”,而是三个更具体的问题:
- 哪一行无法完成原本的操作?
- Python 创建了什么类型的异常对象?
- 这个异常被哪个
except接住,还是继续向外传递并让程序停止?
这一篇沿着订单数据进入 Python 的路径,亲眼看见正常结果和异常对象怎样在同一行分叉。
同一行代码有两条可能路径
先看最常见的金额转换:
def show_amount(raw_amount):
try:
amount = float(raw_amount)
print(f"转换成功:{amount}")
except ValueError as error:
print(f"转换失败:{type(error).__name__}")
show_amount("129.0")
show_amount("未知")
输出:
转换成功:129.0
转换失败:ValueError
两次调用都执行 amount = float(raw_amount),只是输入不同。float("129.0") 能产生数值,float("未知") 无法完成转换,于是没有创建 amount,而是创建一个 ValueError 异常对象。
amount = float(raw_amount)raw_amount = “129.0”raw_amount = “未知”- 01 · 转换
float(“129.0”) - 02 · 产生值
amount → 129.0 - 03 · 下一行
print(…)继续执行 try 内的后续代码
- 01 · 转换失败
float(“未知”) - 02 · 产生异常
ValueError 对象 - 03 · 改变去向
寻找匹配的 excepttry 内后续代码不再执行
异常不是一段模糊的“失败状态”。它是带有类型和错误信息的对象,会改变代码的正常执行顺序。发生异常后,Python 不会从失败行的下一行继续,而是开始寻找处理器。
三种错误分别发生在数据管道的哪一步
订单从文件进入脚本,通常会经过三个边界:
from pathlib import Path
import json
def load_orders(path):
text = Path(path).read_text(encoding="utf-8")
orders = json.loads(text)
for order in orders:
order["amount"] = float(order["amount"])
return orders
orders = load_orders("orders.json")
这段代码中,三种异常不会同时出现。程序先遇到哪一步的问题,就先在那里离开正常路径。
- 01 · 读取文件
Path(path).read_text(…)输入orders.json 路径出错位置FileNotFoundError路径不存在,连文件文本都没有得到。
- 02 · 解析 JSON
json.loads(text)输入文件中的字符串出错位置JSONDecodeError文本不是合法 JSON,列表和字典还没有建立。
- 03 · 转换金额
float(order[“amount”])输入某条订单的 amount出错位置ValueErrorJSON 已解析,但这个字段不能解释为小数。
return orders调用方才能拿到完整订单列表三个名字不能互换:
| 异常类型 | 失败的操作 | 先检查什么 |
|---|---|---|
FileNotFoundError | 打开指定路径 | 路径、文件名、运行目录 |
JSONDecodeError | 把文本解析成 JSON | 括号、引号、逗号和文件内容 |
ValueError | 把已有字段转换成目标数值 | 原始字段值和允许的格式 |
调试时先读 traceback 最后一行的异常类型和信息,再向上找到第一处属于自己代码的文件名与行号。不要只看到 ValueError 就随便包一层 try;先确认究竟是哪一个原始值触发了它。
异常对象怎样寻找匹配的 except
try 可以跟多个 except。Python 会拿着当前异常对象,从上到下检查类型:
from pathlib import Path
import json
try:
text = Path("orders.json").read_text(encoding="utf-8")
orders = json.loads(text)
for order in orders:
order["amount"] = float(order["amount"])
except FileNotFoundError as error:
print(f"输入文件不存在:{error.filename}")
except json.JSONDecodeError as error:
print(f"JSON 格式错误:第 {error.lineno} 行")
print("文件读取阶段结束")
JSONDecodeError 对象存在匹配处理器- 01 · 检查
except FileNotFoundError类型不匹配,继续向下找 - 02 · 命中
except json.JSONDecodeError as error变量 error 指向这个异常对象 - 03 · 处理
print(“JSON 格式错误…”)执行匹配处理器中的代码 - 04 · 恢复位置
print(“文件读取阶段结束”)从整个 try / except 之后继续
ValueError 对象当前没有匹配处理器- 01 · 检查
except FileNotFoundError不匹配 - 02 · 检查
except JSONDecodeError不匹配 - 03 · 向外传播
离开当前 try 所在范围交给更外层调用者继续寻找处理器 - 04 · 没人处理
打印 traceback 并停止错误不会被静默隐藏
“接住异常”不表示回到失败行重试。匹配的 except 执行完后,代码会从整个 try / except 结构之后继续。若当前层没有匹配类型,异常就离开这里,交给外层调用者;一直没人接住时,程序显示 traceback 并停止。
批量订单:记录坏数据,继续下一条
下面模拟从外部文件读到的原始订单。金额暂时是文本,A002 的 "未知" 是特意加入的坏数据:
raw_orders = [
{"order_no": "A001", "amount": "129.0", "status": "已支付"},
{"order_no": "A002", "amount": "未知", "status": "已退款"},
{"order_no": "A003", "amount": "199.0", "status": "已支付"},
]
valid_orders = []
valid_order_nos = []
problems = []
parsed_total = 0.0
for order in raw_orders:
try:
amount = float(order["amount"])
except ValueError as error:
problems.append({
"order_no": order["order_no"],
"error_type": type(error).__name__,
})
continue
clean_order = order.copy()
clean_order["amount"] = amount
valid_orders.append(clean_order)
valid_order_nos.append(order["order_no"])
parsed_total += amount
print(valid_order_nos)
print(problems)
print(parsed_total)
输出:
['A001', 'A003']
[{'order_no': 'A002', 'error_type': 'ValueError'}]
328.0
这里的错误处理范围只包住“一条订单的金额转换”。因此 A002 转换失败后,可以记录订单号和异常类型,再用 continue 进入下一轮。A003 不会被 A002 拖住。
- 01 · 当前行
float(“129.0”) - 02 · 转换成功
amount → 129.0 - 03 · 保存
valid_orders += A001 - 04 · 累计
0.0 → 129.0
- 01 · 出错行
float(“未知”) - 02 · 异常对象
ValueError - 03 · 记录
problems += A002 - 04 · continue
跳过本轮余下代码parsed_total 保持 129.0
- 01 · 当前行
float(“199.0”) - 02 · 转换成功
amount → 199.0 - 03 · 保存
valid_orders += A003 - 04 · 累计
129.0 → 328.0
这个示例处理的是原始数据校验,所以每条记录都尝试解析金额,状态字段暂时不参与筛选。后续业务统计再决定“已支付”或“已退款”分别怎样处理。
什么时候跳过,什么时候应该停止
能不能继续,不取决于“异常看起来严重不严重”,而取决于剩余数据是否仍然可以被正确理解:
| 情况 | 影响范围 | 推荐动作 |
|---|---|---|
| 一条订单金额无法转换,其他订单彼此独立 | 单条记录 | 记录订单号与异常类型,跳过当前记录,继续下一条 |
必需的 orders.json 不存在 | 整个输入 | 明确报告路径并停止本次任务 |
| 整份 JSON 无法解析 | 整个文件结构 | 报告解析位置并停止处理这份文件 |
| 出现没有预期过的异常类型 | 代码或假设可能有问题 | 让异常暴露并修复,不要静默当作坏记录 |
错误处理的范围要尽量贴近你真正能做出决定的位置:单条字段转换可以放在循环内部;整份文件读取和解析则放在循环外部。这样不会因为一个字段坏了就停止全部订单,也不会在整份文件都不可读时假装返回空结果。
不要用无边界的 except Exception 静默吞错
下面的写法会把拼写错误、字段错误和程序逻辑错误一起藏起来:
try:
amount = float("未知")
except Exception:
pass
pass 以后既没有结果,也没有订单号、异常类型和失败位置。脚本可能“看起来跑完了”,输出却已经不可信。
优先捕获当前操作能够预期并处理的具体类型,例如转换金额时捕获 ValueError。如果暂时无法恢复,保留 traceback 让问题暴露,比静默继续更安全。
动手练习:改变坏数据的位置
复制批量订单示例,先不运行,完成以下预测:
- 把 A002 的
amount改成"88.0",三轮各走哪条路径,problems和parsed_total最终是什么? - 把 A003 的
amount改成"一百九十九",哪一轮产生ValueError,前两轮结果是否保留? - 删除
continue,观察坏记录后面的clean_order为什么不应该继续执行。 - 把
except ValueError错写成except FileNotFoundError,说明异常为什么会向外传播。 - 给
orders.json一个不存在的路径:这时应该跳过一条记录,还是停止整批?说明判断依据。