第 5 章 · 异常¶
异常处理是 Python 的常规控制流,地位远比 Java 高。两个核心差异要先记住:Python 没有 checked exceptions(全是"unchecked"),且推崇 EAFP("先做,错了再说")而非 Java 习惯的 LBYL("先检查再做")。
5.1 异常体系与抛出¶
Python 所有异常继承自 BaseException,绝大多数你写的继承 Exception。
常见内置异常对照:
| Python | Java 类比 | 何时抛 |
|---|---|---|
ValueError |
IllegalArgumentException |
值的类型对但内容非法 |
TypeError |
类型/操作不兼容(近似 IllegalArgumentException) |
"a"+1、调用不可调用对象 |
KeyError |
(Map 找不到键) | dict[key] 键不存在 |
IndexError |
IndexOutOfBoundsException |
索引越界 |
FileNotFoundError |
FileNotFoundException |
文件不存在 |
AttributeError |
(字段/方法不存在) | 访问不存在的属性 |
ZeroDivisionError |
ArithmeticException |
除以零 |
5.2 try/except:捕获¶
-
Java
-
Python
要点:
except不是catch;可捕获多个类型:except (KeyError, IndexError):。as绑定异常对象:except ValueError as e: print(e)。- 裸
except:(不写类型)捕获一切——强烈不推荐(连KeyboardInterrupt都吞掉,会导致Ctrl+C失效)。至少写except Exception:。
5.3 else 与 finally:Python 比 Java 多一个子句¶
完整结构:
try:
result = do_work() # 可能出错
except ValueError:
handle_error() # 出错才走
else:
use(result) # ✅ 没出错才走(Python 独有)
finally:
cleanup() # 总是走
else 的意义:把"成功路径"从 try 里分离出来——这样 use(result) 里如果出错,不会被同层的 except ValueError 意外捕获。Java 没有这个子句,容易把成功代码塞进 try 导致误捕。
Pythonic 写法
有 else 就用它把成功路径和可能抛异常的代码分开,避免"过度捕获"。
5.4 没有 checked exceptions ⚠️¶
这是最大的思维转变:
-
Java
-
Python
Python 的异常全是 unchecked(相当于全是 RuntimeException)——没有 throws 声明、没有编译期强制处理。
⚠️ Java 程序员的陷阱
你失去了"编译器提醒我这个函数可能抛哪些异常"的安全网。补救办法:
- 靠文档字符串写明
Raises:段落。 - 靠类型注解:返回
T | None或用Raises文档表达可能失败(部分团队用Result类型)。 - 靠测试覆盖错误路径。
5.5 EAFP vs LBYL¶
两种风格:
-
LBYL(Java 习惯)
-
EAFP(Python 习惯)
EAFP = Easier to Ask Forgiveness than Permission。Python 社区倾向 EAFP,因为它:
- 避免检查与使用之间的竞态(TOCTOU);
- 代码更直白("做就完了");
- 利用异常机制本身。
但也别走极端——EAFP 适合"异常确实罕见"的场景。如果失败是常态(如循环里大量 KeyError),用 dict.get() 之类避免性能损耗和滥用异常做控制流。
5.6 重新抛出与异常链¶
重新抛出当前异常(裸 raise):
异常链(raise ... from)保留原始原因,调试时能看完整调用链:
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError("invalid config") from e # 链式:原始异常作为 __cause__
5.7 自定义异常¶
-
Java
-
Python
Pythonic 写法
- 自定义异常继承
Exception(不要继承BaseException,那是给KeyboardInterrupt这类系统级用的)。 - 给异常起名带
Error后缀(如MyError),和标准库一致。 - 设计异常层次(
AppError->PaymentError->InsufficientFundsError),便于上层按粒度捕获。
本章练习¶
练习 5.1
把下面 LBYL 代码改写成 EAFP,并说明 EAFP 版本如何避免竞态:
参考答案
exists() 与 open() 之间存在 TOCTOU 竞态(文件可能在检查后被删/创建)。EAFP 直接尝试,以"打开是否成功"为唯一真相,天然避免竞态。
练习 5.2
写一个自定义异常层次:AppError(Exception) -> ValidationError(AppError)。再写一个函数 parse_age(s),把字符串转成 int,若为负数抛 ValidationError,并保持原始异常链(若 int(s) 失败)。
参考答案
练习 5.3
解释 except: 和 except Exception: 的区别,以及为什么后者更安全。
参考答案
裸 except: 捕获 BaseException 的所有子类,包括 KeyboardInterrupt(Ctrl+C)和 SystemExit(sys.exit),会让程序无法正常中断/退出。except Exception: 只捕获普通异常,保留系统控制异常的传播,是安全的标准做法。
上一章:第 4 章 · OOP | 下一章:第 6 章 · 模块与包。