← [ BACK_TO_NOTES ]

Python 错误处理

Python 以异常为主错误路径:try/except/else/finally、捕获粒度、raise from 因果链,以及何时翻译异常、何时让其冒泡。

#concept #growing #dev #python #python

[!info] related notes

Python 错误处理

这个概念为什么出现

真实系统里失败是常态:

  • 用户输入不是整数
  • 余额不足
  • 下游 JSON 坏了
  • 磁盘满了

若没有统一错误模型,代码会变成:

  • 到处 if err != None 风格却又不彻底
  • print 后继续跑,数据写一半
  • 捕获过宽,把 KeyboardInterrupt 和业务失败一锅炖
  • 日志只有 something failed,没有因果链

Python 选择:异常是常规失败路径的一等机制。成功走返回值,失败抛出,由合适的边界捕获、翻译或记录。

这与 Go “error 值显式返回”不同,但目标一致——让失败可见、可分类、可清理。

[!abstract] 一句话理解 用 try/except/else/finally 管理失败控制流;捕获要具体;清理放 finally 或上下文管理器;跨边界翻译错误时用 raise ... from ... 保留根因。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:积分兑换既要防坏输入,也要表达业务失败

兑换服务收到字符串形式的积分花费:

  • 解析失败 → 输入错误(可映射 HTTP 400)
  • 余额不足 → 业务错误(可映射 409/400 业务码)
  • 无论成败 → 写审计收尾(对应 finally)
# points_redeem_errors.py
# 业务意图:兑换积分,区分输入错误与业务失败,并保证审计收尾。
# 教学点:
# - 捕获具体异常;
# - else 仅成功路径;
# - 正常 Python 控制流离开 try 时执行 finally;
# - raise from 保留因果。

from __future__ import annotations


class InsufficientPointsError(Exception):
    """余额不足:可识别的业务失败。"""


def parse_points(raw: str) -> int:
    # 输入边界:必须是正整数。
    points = int(raw)  # 非数字 -> ValueError
    if points <= 0:
        raise ValueError("points must be positive")
    return points


def redeem(balance: int, raw_cost: str) -> int:
    print("audit: start")
    try:
        cost = parse_points(raw_cost)
        if cost > balance:
            raise InsufficientPointsError(f"need {cost}, have {balance}")
    except ValueError as exc:
        # 翻译为统一输入错误,同时保留 cause 便于排障。
        raise ValueError(f"invalid cost: {raw_cost!r}") from exc
    else:
        # 仅当 try 未抛异常:执行扣减。
        new_balance = balance - cost
        print(f"audit: success cost={cost}")
        return new_balance
    finally:
        # 成功、业务失败、输入失败都会到这里。
        print("audit: end")


def main() -> None:
    print("case ok:", redeem(100, "30"))

    try:
        redeem(100, "x")
    except ValueError as exc:
        print("case bad input:", exc)
        print("  cause:", type(exc.__cause__).__name__)

    try:
        redeem(10, "30")
    except InsufficientPointsError as exc:
        print("case business:", exc)


if __name__ == "__main__":
    main()

建议运行:

python points_redeem_errors.py

期望输出:

audit: start
audit: success cost=30
audit: end
case ok: 70
audit: start
audit: end
case bad input: invalid cost: 'x'
  cause: ValueError
audit: start
audit: end
case business: need 30, have 10

(audit: end 在失败路径也会打印;业务失败路径同样有 start/end。)

结合场景再看四个关注点

  1. 类型区分失败模式:输入 vs 余额不足,调用方可分别映射。
  2. else 避免把成功逻辑放进 try 导致误捕获。
  3. finally 在正常返回或异常退出时运行;这里的 print 仅演示顺序,不是持久审计。强制终止进程、退出前未完成的输出等不受此保证。
  4. raise from 让日志能挖到 int('x') 根因。

核心概念与准确模型

1. 语句结构

try:
    ...
except SomeError as exc:
    ...
except AnotherError:
    ...
else:
    ...   # try 正常执行到末尾,未由 return/break/continue 提前离开
finally:
    ...   # 几乎总是执行(清理)

执行直觉:

flowchart TD
  T[try 体] -->|成功| E[else]
  T -->|SomeError| X1[except SomeError]
  T -->|其它异常| P[向外传播]
  E --> F[finally]
  X1 --> F
  P --> F
  F --> R[继续/传播]

2. 异常是对象

3. 抛出

写法含义
raise ValueError("...")抛新异常
raise在 except 中原样重抛
raise New from exc显式因果链 __cause__
raise New from None抑制默认 traceback 中的隐式上下文展示,不删除 __context__

4. 捕获粒度

做法评价
具体类型首选
except Exception边界兜底或明确的恢复策略;避免吞掉未知错误,是否记录由分层策略决定
裸 except:会捕获退出/中断等;通常避免,必要的清理后应重抛
except BaseException几乎不要(含退出/取消类)

5. EAFP vs LBYL

  • EAFP(Easier to Ask Forgiveness than Permission):先做,不行再捕——Pythonic 常见风格
  • LBYL(Look Before You Leap):先检查再做——适合检查便宜且避免异常热路径

例:字典键用 in/get 往往比依赖 KeyError 更清晰;文件存在检查则有 TOCTOU 竞态,EAFP 打开更稳。

6. 与上下文管理器

资源清理优先:

with open(path) as f:
    ...

而不是手写层层 try/finally。见 上下文管理器。

设计动机

  1. 失败不沉默:异常打断正常返回,迫使边界处理或崩溃暴露问题
  2. 类型可分类:比布尔失败位更易分支
  3. 栈轨迹可观测:配合 logging.exception
  4. 开放扩展:用户可定义领域异常体系

代价:滥用异常做常规分支会难读、偏慢;捕获过宽会隐藏缺陷。

边界情况与反直觉行为

1. finally 中 return

会掩盖 try 的 return/异常——极度不推荐。

2. 异常发生在 except 或 finally

可能改变传播中的异常;需要刻意设计。

3. else 的价值

把“成功后逻辑”放 else,避免被同一 try 的 except 误伤。

4. 生成器与 throw/close

生成器清理与 GeneratorExit 有特殊路径(进阶)。

5. asyncio 取消

asyncio.CancelledError 自 Python 3.8 起继承 BaseException;不要与仍继承 Exception 的 concurrent.futures.CancelledError 混淆。见 任务、超时与取消。

6. 性能

异常路径比分支重;不要用异常做超热路径的期望控制流(如逐字符解析)。

常见误区

[!warning] 常见误区:捕获后 pass 错误理解:程序不崩就是成功。
正确模型:至少记录、翻译或返回明确领域错误。

[!warning] 常见误区:所有失败都 ValueError/Exception 错误理解:省类型。
正确模型:调用方需要分支的失败应可区分。

[!warning] 常见误区:用返回 None 表示一切失败 错误理解:简单。
正确模型:None 与失败混淆;缺值用 Optional,失败用异常或 Result 型设计。

[!warning] 常见误区:在底层库 basicConfig + 吞异常 错误理解:对用户好。
正确模型:库少捕多抛;应用边界统一翻译与日志。

[!warning] 常见误区:把 Go 的 error 值思维硬套成“禁止异常” 错误理解:异常不 Pythonic。
正确模型:Python 标准库与生态以异常为主;关键是边界策略,不是消灭异常。

与相邻概念对比

文化主路径典型
Python 异常抛/捕本篇
Go error 值返回 errorgo-error-handling
Result 类型Ok/Err 容器某些函数式库
HTTP 状态码边界映射Web 层

文字说明:在服务边界,把领域异常映射为稳定对外错误码;在内部,保留类型与因果链。

工程实践

分层策略

  1. 核心域:抛领域异常,少捕获
  2. 适配层(HTTP/CLI):捕获 → 日志 → 用户信息/状态码
  3. 基础设施:翻译第三方异常,保留 from

日志

logger.exception("redeem failed user=%s", user_id)

在 except 中调用会记录当前异常堆栈;对外消息与对内细节分离,避免在每层重复记录同一次错误。

测试

  • 成功路径
  • pytest.raises(ValueError)
  • 对链式异常可断言 __cause__

API 映射示例(思想)

异常HTTP
已确认来自输入校验的 ValueError400;未知内部 ValueError 不应一律归为客户端错误
InsufficientPointsError409 或业务码
未知 Exception500 + 关联 ID

可验证实验

实验 1:成功路径

redeem(100, "30") == 70,日志含 success。

实验 2:坏输入

redeem(100, "x") 抛 ValueError,__cause__ 存在。

实验 3:业务失败

redeem(10, "30") 抛 InsufficientPointsError,仍有 audit: end。

实验 4:else vs 放入 try

把 new_balance = ... 放进 try,并故意让其后代码抛 ValueError,观察是否被输入错误分支误伤。

实验 5:裸 except 的危险(只在沙箱)

用 except: 捕一切,尝试 Ctrl+C 行为(交互环境)——理解为何禁止。

本节总结

  • Python 错误处理的质量 = 分类 + 清理 + 因果 + 边界翻译。
  • try/except/else/finally 要按职责拆,而不是一个大 try 包全世界。
  • 与 Go 不同路径,同一工程目标:失败可处理、可观测、不破坏资源。

自测题

概念题

  1. else 与把代码放在 try 末尾的关键差别?
  2. 何时自定义 InsufficientPointsError 而不是统一 ValueError?

代码推理题

  1. finally 是否在 return 之前执行?

工程思考题

  1. 库代码捕获 Exception 并返回 None 有什么长期成本?
参考答案
  1. else 中的异常不会被同一 try 的 except 捕获。
  2. 当调用方需要区分输入错误与业务余额不足并走不同处理时。
  3. return 表达式先求值,再执行 finally,最后才真正返回;finally 中的 return/异常可能覆盖原结果。
  4. 失败被静默、调用方无法分支、排障丢失堆栈与类型。

延伸阅读与资料来源

资料类型支撑内容
Errors and Exceptions教程入门
Exception hierarchy文档内置树
raise规范raise/from
EAFP glossary术语EAFP

笔记元信息

  • 角色:原子概念(错误模型主叙事)
  • 深度对齐:go-error-handling 的工程完整度
  • 专篇:层次/链式、上下文管理器、asyncio 取消
创建于 2026/6/6 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本