
九块路线总览见《Python 到 FastAPI 九层进阶》。第五块把代码和文件组织成了工程,但工程一旦接触网络、磁盘和外部输入,失败就成为正常分支。真正的问题是:出错后还能否知道发生了什么、影响到哪里?
老李见过一次难查的夜间故障:底层把连接错误吞掉并返回空列表,上层误以为当天没有订单。异常不是多余噪声,它是调用方做决策所需的信号。
这一块的验收标准是:能划定捕获范围、保留异常因果,用结构化日志定位现场,并通过调试与检查缩短复现路径
这一块按“捕获—分类—转换—记录—调试—静态检查”推进。目标不是消灭异常,而是让每次失败都落到明确责任层。
官方概念
try 包住可能失败的最小操作,except 处理匹配异常,else 只在无异常时执行,finally 无论成败都执行,适合释放资源。
类比记忆
像机场分流:安检异常走处置通道,通过后进入登机区,最后所有旅客都要经过出口核验。
events = []
try:
order_id = int("7")
except ValueError:
events.append("格式错误")
else:
events.append(f"解析成功:{order_id}")
finally:
events.append("已清理")
print(events)
运行输出:
['解析成功:7', '已清理']
使用边界
不要用裸 except 混淆退出信号和业务错误;finally 中的 return 会遮蔽原异常。资源对象优先交给上下文管理器。

官方概念
异常类型形成继承层级;多个 except 从上到下匹配,因此具体类型必须写在宽泛父类之前。调用方应按能够采取的恢复动作分类。
类比记忆
像分诊台先识别具体科室,再交给综合诊室;若综合入口放最前,后面的专科规则永远没有机会。
errors = [ValueError("参数"), KeyError("编号")]
for error in errors:
try:
raise error
except KeyError:
print("查找错误")
except ValueError:
print("参数错误")
运行输出:
参数错误
查找错误
使用边界
Exception 适合作为进程或任务边界的兜底并记录后重新抛出,不应遍布业务代码。KeyboardInterrupt 等不属于普通业务异常。

官方概念
raise 可主动报告契约失败;自定义异常表达领域语义;raise NewError(...) from exc 将底层原因挂到 __cause__,便于日志和追踪。
类比记忆
像客服把“数据库唯一键冲突”翻译成“订单号已存在”,同时保留后台工单号,前台能理解,后台能追根。
class OrderError(Exception):
pass
try:
int("A-7")
except ValueError as exc:
try:
raise OrderError("订单号必须是整数") from exc
except OrderError as error:
print(error)
print(type(error.__cause__).__name__)
运行输出:
订单号必须是整数
ValueError
使用边界
只在抽象边界转换异常,并保留可诊断上下文;日志不得泄露令牌、密码和完整个人数据。无需转换时用裸 raise 保留原回溯。

官方概念
日志级别表达严重度,Logger 决定事件来源,Handler 决定去向,Formatter 决定呈现。请求标识、任务标识等上下文应贯穿调用链。
类比记忆
像快递轨迹:每个节点都记录同一运单号、时间和动作,排查时才能把分散事件串成一条路径。
import logging
record = logging.LogRecord(
"orders", logging.INFO, __file__, 8,
"order=%s status=%s", ("A7", "paid"), None
)
formatter = logging.Formatter("%(levelname)s %(name)s %(message)s")
print(formatter.format(record))
运行输出:
INFO orders order=A7 status=paid
使用边界
库代码不要擅自配置根日志器;高频路径要控制日志量。生产环境优先使用稳定字段,避免只写无法聚合的自然语言。

官方概念
breakpoint() 默认进入 pdb,可检查栈帧、变量并单步执行;IDE 调试器在同一调试协议上提供断点、条件和变量面板。
类比记忆
像在流水线上设置检查站:无需猜整条线哪里出错,只观察关键节点进入前后的状态变化。
import pdb
def average(total: int, count: int) -> float:
# 排障时可在下一行临时放置 breakpoint()
return total / count
print(average(12, 3))
print(hasattr(pdb, "post_mortem"), callable(breakpoint))
运行输出:
4.0
True True
使用边界
不要把活动断点提交到生产路径;远程进程暂停可能阻塞请求。无法交互时保留日志、回溯、输入摘要和最小复现。

官方概念
assert 检查开发期内部不变量;类型注解给静态检查器提供契约,但 Python 默认不会在运行时强制类型。两者都不能替代输入校验和测试。
类比记忆
像施工图与现场传感器:图纸提前发现接口不合,传感器检查运行状态,门禁仍要独立验证外来输入。
def ratio(done: int, total: int) -> float:
assert total > 0, "总数必须为正"
return done / total
value: float = ratio(3, 4)
print(f"{value:.0%}")
print(ratio.__annotations__)
运行输出:
75%
{'done': <class 'int'>, 'total': <class 'int'>, 'return': <class 'float'>}
使用边界
优化模式可移除 assert,因此不能用它校验用户权限或外部输入。类型检查需要 mypy、Pyright 等工具进入持续集成。

事实:异常会沿调用栈传播,直到被匹配处理。
因果链:过早吞错 → 上层失去事实 → 错误状态继续流动。
影响:在任务、请求和进程边界统一记录,可隔离影响并保留证据。
边界:能够安全恢复的局部错误仍应就近处理。

事实:一次请求常跨越多个模块与后台任务。
因果链:日志彼此孤立 → 时间相近事件混杂 → 根因定位变慢。
影响:稳定的请求或任务标识能串联完整轨迹。
边界:标识本身不能包含敏感业务数据。

事实:输入、环境和执行路径共同决定故障。
因果链:直接修改生产代码 → 变量继续增加 → 结论无法验证。
影响:缩减案例并固定依赖后,每个假设都能被单独证伪。
边界:并发时序问题还需保留调度和时间证据。

异常与返回值:异常表示正常返回契约无法完成,返回值表达成功结果;raise 与 raise exc:前者在处理块中保留原回溯,后者会改变回溯位置;日志与调试器:日志留下跨时间证据,调试器暂停并检查当前状态;断言与输入校验:断言守内部不变量,输入校验面对不可信数据。
可靠错误处理从精确捕获开始:转换时保留因果,记录时携带关联标识,排障时用回溯与最小复现验证假设。断言、类型检查和测试各守一层,不能互相替代。下一块将处理多个任务同时推进时的新风险。
