程序不可能”永远走 happy path”。用户会输入奇怪的数据、网络会断、文件会不存在、磁盘会满、依赖会挂——这些都会让程序抛出异常。Python 用 try/except 处理异常,规则简单,但用好并不容易。这一篇讲清楚异常的语法、几种典型模式、”要 catch 还是让它挂”的判断、以及 Python 3.11+ 的一些新语法。
一、什么是异常
异常是”运行时出错时抛出的对象”:
1 | 1 / 0 |
Python 里所有异常都是 BaseException 的子类。日常业务代码几乎只 catch Exception 及其子类。
二、基本语法
1 | try: |
执行顺序:
- 运行 try 块;
- 如果抛异常 → 从上到下匹配 except;
- 如果没抛异常 → 执行 else;
- 无论走哪条路 → 执行 finally。
三、抓具体异常,不要裸 except
1 | # ❌ 万恶之源 |
问题:
- 遮蔽真正的 bug;
- 屏蔽 Ctrl+C(因为 KeyboardInterrupt 也是异常);
- 让调试变噩梦。
规则:
- 尽量抓具体的异常(
except ValueError); - 不确定时
except Exception(不会吞 KeyboardInterrupt/SystemExit); - 永远不写裸
except:。
四、只 catch 你能处理的
能处理的意思是”知道怎么办”——重试、返回默认值、给用户友好提示、清理状态。如果你只是想”让程序不崩”,那不是处理,那是掩盖 bug。
1 | # ❌ 反面:只是想不崩 |
五、抛异常:raise
1 | def divide(a, b): |
raise ... from e 让 traceback 显示”是从这个原因引发的”,调试非常清晰。
六、常见内置异常
| 异常 | 意思 |
|---|---|
ValueError |
值不对(例:int("abc")) |
TypeError |
类型不对(例:"a" + 1) |
KeyError |
字典 key 不存在 |
IndexError |
列表索引越界 |
AttributeError |
属性不存在 |
NameError |
变量不存在 |
FileNotFoundError |
文件不存在 |
PermissionError |
权限不足 |
IOError / OSError |
IO / 系统错误 |
ZeroDivisionError |
除零 |
TimeoutError |
超时 |
StopIteration |
迭代结束 |
KeyboardInterrupt |
Ctrl+C |
RuntimeError |
通用运行时错误 |
NotImplementedError |
抽象方法未实现 |
AssertionError |
assert 失败 |
七、异常的继承结构
1 | BaseException |
推论:except LookupError 能同时抓 KeyError 和 IndexError;except OSError 能抓所有 IO 相关。
八、EAFP vs LBYL
Python 的哲学是 “Easier to Ask Forgiveness than Permission”(EAFP)——先做,出错再处理:
1 | # ✅ EAFP(Pythonic) |
好处:
- EAFP 只在异常路径慢,正常路径快;
- 避免 TOCTOU 问题(检查 → 使用之间状态变了);
- 代码更直接。
但也不是所有场景都 EAFP。当”检查便宜且大概率失败”时 LBYL 更好:
1 | if not items: |
九、finally:清理资源
finally 无论有没有异常都会执行,适合清理:
1 | f = open("file.txt") |
但更 Pythonic 的做法是用 with:
1 | with open("file.txt") as f: |
with 用的是上下文管理器 的机制。
十、else 子句
try/else 用得少,但语义清晰:
1 | try: |
规则:把不该抛异常的代码放 else,try 里只放”可能抛”的那句。
十一、异常链:raise ... from
想”包装”底层异常成业务异常,同时保留 traceback:
1 | class DatabaseError(Exception): |
调用方看到:
1 | IOError: connection reset |
一眼就能看到”表面上抛 DatabaseError,其实底层是 IOError”。
raise ... from None 可以隐藏原因,避免长链暴露实现细节。
十二、Python 3.11+ 的新语法
1. except*:异常组
Python 3.11 引入了 ExceptionGroup,用来同时处理多个异常(并发场景很有用):
1 | try: |
普通业务不用管,遇到 asyncio.TaskGroup 会用到。
2. add_note
异常可以携带额外备注:
1 | try: |
日志里会显示这条备注,无需重新包装异常。
十三、assert:不是异常处理
1 | assert x > 0, "x 必须为正" |
assert 用来校验假设——不该出现的情况。别用它做业务校验!因为 Python 加 -O 参数就会跳过所有 assert。
规则:
- 输入验证 →
raise ValueError; - 内部不变量 →
assert; - 生产环境的关键校验 →
if not ...: raise。
十四、常见异常模式
1. 重试
1 | import time |
2. 默认值
1 | def get_config(key, default=None): |
3. 转换异常
1 | try: |
4. 日志 + 重抛
1 | try: |
logger.exception 是日志里最有用的方法之一——自动包含完整 traceback。
十五、一个完整例子:健壮的输入读取
1 | # safe_input.py |
十六、常见陷阱
- 裸
except::吞掉一切,包括 Ctrl+C。永远不要写。 except Exception: pass:跟裸 except 差不多糟,除非你真的知道自己在干嘛。except Exception as e: raise e:丢失原始 traceback。要raise(不带 e)。- 在 try 里放太多代码:只把”可能抛”的一句放 try,其他放 else。
assert做业务校验:-O参数下失效。用raise。- 异常链爆炸:无脑
raise ... from e让 traceback 特别长。有时用from None隐藏。 - finally 里 return:会覆盖 try/except 里的 return,谨慎使用。
十七、小结与延伸阅读
try/except/else/finally四件套;- 抓具体异常,别裸 except;
- Python 提倡 EAFP:先做,出错再处理;
- 清理资源优先用
with,其次finally; raise ... from e保留原因,raise ... from None隐藏;assert用于内部假设,不做业务校验;- 日志记异常用
logger.exception。
延伸阅读:
- Python 异常官方文档:https://docs.python.org/zh-cn/3/tutorial/errors.html
- PEP 654 ExceptionGroup:https://peps.python.org/pep-0654/
下一篇 自定义异常与最佳实践 我们讲怎么设计自己的异常层次。