2.26. 抛出错误¶
函数可以通过抛出异常来向其调用者发出问题信号。关键字是 raise:
def square_root(x):
if x < 0:
raise ValueError("square_root expects a non-negative number")
return x ** 0.5
调用 square_root(-1) 会在 raise 那一行停下,跳出函数,并在调用者中寻找匹配的 except。如果没有任何调用者捕获它,脚本就会带着回溯信息结束。
2.26.1. 为什么要抛出异常而不是返回一个哨兵值¶
报告“无效输入”有两种方式:
# signal with a sentinel
def square_root_or_none(x):
if x < 0:
return None
return x ** 0.5
# raise an exception
def square_root(x):
if x < 0:
raise ValueError("...")
return x ** 0.5
异常这种形式通常更好:
调用者必须有意识地处理错误情况 —— 要么用
try,要么让异常向外传播。哨兵值容易被遗忘,也容易被误当作正常结果。错误消息会随异常一同传递;而哨兵值的做法必须把诊断信息附加到别处。
对于未处理的异常,默认行为是带着回溯信息的明显崩溃,且回溯会指向出问题的那次调用。而默默返回
None之后会变成隐蔽的 bug。
只有当“未找到”是一种常规的、非异常的结果时才使用哨兵值 —— dict.get() 在键缺失时返回 None,正是因为查找有时本就预期会落空。
2.26.2. 自定义异常类¶
要抛出一个调用者可能希望与内置错误区分开来的问题,请定义一个 Exception 的子类:
class ConfigError(Exception):
pass
def load_config(path):
try:
f = open(path)
except OSError as e:
raise ConfigError("missing config file: " + path)
try:
load_config("settings.json")
except ConfigError as e:
print("startup failed:", e)
空的 class 体没有问题 —— 重要的是名称本身,因为调用者是按类来捕获的。如果某个调用者可能希望在一个块中捕获整个一族错误,就把相关的错误归到一个共同的基类下。
2.26.2.1. 重新抛出¶
在 except 块内部使用一个裸 raise 会重新抛出当前异常,使其传播到下一个处理程序:
try:
do_work()
except Exception as e:
log(e)
raise # let it keep going
当一个函数想要观察某个错误(记录它、计数、撤销一次部分更改)而又不真正处理它时,这正是恰当的写法。
2.26.3. 何时捕获、何时传播¶
一条有用的经验法则:
在能够有意义地恢复的层级捕获异常 —— 替换为默认值、重试、跳过无效输入。
当除了崩溃之外没有任何有用的事可做时,或者当上一层才知道如何恢复时,就让它向外传播。
调用栈中间的某个函数若吞掉每一个错误并悄悄返回,会让故障无法追踪。请优先让异常一路传播,直到抵达真正有应对方案的代码。