系列 · Python 零基础到简单脚本 第 8 篇 / 共 11 篇 教程

Python 错误与调试图解:异常在哪里发生,又去了哪里

沿文件读取、JSON 解析和金额转换的运行路径,图解异常发生位置、except 匹配与向外传播,以及批量订单怎样记录坏数据并继续。

作者:黄撑 更新于 2026-08-27
本文目录 7 节 · 点击展开

Python 报错时,真正需要回答的不是“怎么让红字消失”,而是三个更具体的问题:

  1. 哪一行无法完成原本的操作?
  2. Python 创建了什么类型的异常对象?
  3. 这个异常被哪个 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 = “未知”
正常路径
  1. 01 · 转换float(“129.0”)
  2. 02 · 产生值amount → 129.0
  3. 03 · 下一行print(…)继续执行 try 内的后续代码
异常路径
  1. 01 · 转换失败float(“未知”)
  2. 02 · 产生异常ValueError 对象
  3. 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")

这段代码中,三种异常不会同时出现。程序先遇到哪一步的问题,就先在那里离开正常路径。

外部文件 → 文本 → Python 对象 → 数值字段异常类型直接告诉你哪一种操作没有完成
  1. 01 · 读取文件Path(path).read_text(…)
    输入orders.json 路径
    出错位置FileNotFoundError

    路径不存在,连文件文本都没有得到。

  2. 02 · 解析 JSONjson.loads(text)
    输入文件中的字符串
    出错位置JSONDecodeError

    文本不是合法 JSON,列表和字典还没有建立。

  3. 03 · 转换金额float(order[“amount”])
    输入某条订单的 amount
    出错位置ValueError

    JSON 已解析,但这个字段不能解释为小数。

只有三步全部成功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 对象存在匹配处理器
  1. 01 · 检查except FileNotFoundError类型不匹配,继续向下找
  2. 02 · 命中except json.JSONDecodeError as error变量 error 指向这个异常对象
  3. 03 · 处理print(“JSON 格式错误…”)执行匹配处理器中的代码
  4. 04 · 恢复位置print(“文件读取阶段结束”)从整个 try / except 之后继续
ValueError 对象当前没有匹配处理器
  1. 01 · 检查except FileNotFoundError不匹配
  2. 02 · 检查except JSONDecodeError不匹配
  3. 03 · 向外传播离开当前 try 所在范围交给更外层调用者继续寻找处理器
  4. 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 拖住。

逐条处理 · 一条坏记录不等于整批不可用A002 进入问题清单,A003 继续走正常路径
第 1 轮A001 · 已支付raw amount: “129.0”
  1. 01 · 当前行float(“129.0”)
  2. 02 · 转换成功amount → 129.0
  3. 03 · 保存valid_orders += A001
  4. 04 · 累计0.0 → 129.0
第 2 轮A002 · 已退款raw amount: “未知”
  1. 01 · 出错行float(“未知”)
  2. 02 · 异常对象ValueError
  3. 03 · 记录problems += A002
  4. 04 · continue跳过本轮余下代码parsed_total 保持 129.0
第 3 轮A003 · 已支付raw amount: “199.0”
  1. 01 · 当前行float(“199.0”)
  2. 02 · 转换成功amount → 199.0
  3. 03 · 保存valid_orders += A003
  4. 04 · 累计129.0 → 328.0
有效记录A001 · A003
问题记录A002 · ValueError
已解析金额328.0

这个示例处理的是原始数据校验,所以每条记录都尝试解析金额,状态字段暂时不参与筛选。后续业务统计再决定“已支付”或“已退款”分别怎样处理。

什么时候跳过,什么时候应该停止

能不能继续,不取决于“异常看起来严重不严重”,而取决于剩余数据是否仍然可以被正确理解:

情况影响范围推荐动作
一条订单金额无法转换,其他订单彼此独立单条记录记录订单号与异常类型,跳过当前记录,继续下一条
必需的 orders.json 不存在整个输入明确报告路径并停止本次任务
整份 JSON 无法解析整个文件结构报告解析位置并停止处理这份文件
出现没有预期过的异常类型代码或假设可能有问题让异常暴露并修复,不要静默当作坏记录

错误处理的范围要尽量贴近你真正能做出决定的位置:单条字段转换可以放在循环内部;整份文件读取和解析则放在循环外部。这样不会因为一个字段坏了就停止全部订单,也不会在整份文件都不可读时假装返回空结果。

不要用无边界的 except Exception 静默吞错

下面的写法会把拼写错误、字段错误和程序逻辑错误一起藏起来:

try:
    amount = float("未知")
except Exception:
    pass

pass 以后既没有结果,也没有订单号、异常类型和失败位置。脚本可能“看起来跑完了”,输出却已经不可信。

优先捕获当前操作能够预期并处理的具体类型,例如转换金额时捕获 ValueError。如果暂时无法恢复,保留 traceback 让问题暴露,比静默继续更安全。

动手练习:改变坏数据的位置

复制批量订单示例,先不运行,完成以下预测:

  1. 把 A002 的 amount 改成 "88.0",三轮各走哪条路径,problemsparsed_total 最终是什么?
  2. 把 A003 的 amount 改成 "一百九十九",哪一轮产生 ValueError,前两轮结果是否保留?
  3. 删除 continue,观察坏记录后面的 clean_order 为什么不应该继续执行。
  4. except ValueError 错写成 except FileNotFoundError,说明异常为什么会向外传播。
  5. orders.json 一个不存在的路径:这时应该跳过一条记录,还是停止整批?说明判断依据。