五、异常与错误处理¶
基础知识¶
优先使用异常捕获¶
try 语句常用知识¶
抛出异常,而不是返回错误¶
使用上下文管理器¶
案例故事¶
提前崩溃¶
Q:为什么要捕获异常?
A:在代码中捕获异常,表面上是避免程序因为异常发生而直接崩溃,但它的核心,其实是编码者对处于程序主流程之外的、已知或未知情况的一种妥当处置。而妥当这个词正是异常处理的关键。
不应弄一个庞大的try语句,把所有可能出错、不可能出错的代码,一股脑儿地全部用 except Exception:包裹起来,而应做最精准的异常捕获。
其中,精准捕获包括如下: 1. 永远只捕获那些可能会抛出异常的语句块; 2. 尽量只捕获精确的异常类型,而不是模糊的 Exception; 3. 如果出现了预期外的异常,让程序早点儿崩溃也未必是件坏事。
异常与抽象一致性¶
-
避免抛出抽象级别高于当前模块的异常
-
让模块只抛出与当前抽象级别一致的异常;
-
在必要的地方进行异常包装与转换。
-
包装抽象级别低于当前模块的异常
除了应该避免抛出高于当前抽象级别的异常外,我们同样应该避免泄露低于当前抽象级别的异常。这样做同样是为了保证异常类的抽象一致性。
Example
HTTP 工具库 requests,在请求出错时所抛出的异常,并不是它在底层所使用的 urllib3 模块的原始异常,而是经过 requests.exceptions 包装过的异常
urllib3 模块是 requests 依赖的低层实现细节,而这个细节在未来是有可能变动的。当某天 requests 真的要修改低层实现时,这些包装过的异常类,就可以避免对用户侧的错误处理逻辑产生不良影响。
编程建议¶
不要随意忽略异常¶
面对异常,调用方可以: - 在 except 语句里捕获并处理它,继续执行后面的代码; - 在 except 语句里捕获它,将错误通知给终端用户,中断执行; - 不捕获异常,让异常继续往堆栈上层走,最终可能导致程序崩溃。 - 无论选择哪种方案,都比下例中的直接忽略异常更好。
不要手动做数据校验¶
在日常编码时,很大比例的错误处理工作和用户输入有关。
我们要把“输入数据校验”当作一个独立的领域,挑选更适合的模块来完成这项工作。pydantic 模块是一个不错的数据校验工具。
from pydantic import BaseModel, conint, ValidationError
class NumberInput(BaseModel):
# 使用类型注解 conint 定义 number 属性的取值范围
number: conint(ge=0, le=100)
def input_a_number_with_pydantic():
while True:
number = input('Please input a number (0-100): ')
# 实例化为 pydantic 模型,捕获校验错误异常
try:
number_input = NumberInput(number=number)
except ValidationError as e:
print(e)
continue
number = number_input.number
break
print(f'Your number is {number}')
Tips
在编写代码时,我们应当尽量避免手动校验任何数据。因为数据校验任务独立性很强,所以应该引入合适的第三方校验模块(或者自己实现),让它们来处理这部分专业工作。
Web 应用的数据校验工作通常比较容易。比如 Django 框架有自己的表单验证模块,Flask 也可以使用 WTForms 模块来进行数据校验。
抛出可区分的异常¶
不要使用 assert 来检查参数合法性¶
无须处理是最好的错误处理¶
总结¶
基础知识部分简单介绍了 LBYL 和 EAFP 两种编程风格。
Pythonista 更倾向于使用基于异常捕获的 EAFP 风格。
(1)基础知识
- 一个 try 语句支持多个 except 子句,但请记得把更精确的异常类放在前面
- try 语句的 else 分支会在没有异常时执行,因此它可用来替代标记变量
- 不带任何参数的 raise 语句会重复抛出当前异常
- 上下文管理器经常用来处理异常,它最常见的用途是替代 finally 子句
- 上下文管理器可以用来忽略某段代码里的异常
- 使用 @contextmanager 装饰器可以轻松定义上下文管理器
(2)错误处理与参数校验
- 当你可以选择编写条件判断或异常捕获时,优先选异常捕获(EAFP)
- 不要让函数返回错误信息,直接抛出自定义异常吧
- 手动校验数据合法性非常烦琐,尽量使用专业模块来做这件事
- 不要使用 assert 来做参数校验,用 raise 替代它
- 处理错误需要付出额外成本,假如能通过设计避免它就再好不过了
- 在设计 API 时,需要慎重考虑是否真的有必要抛出错误
- 使用“空对象模式”能免去一些针对边界情况的错误处理工作
(3)当你捕获异常时:
- 过于模糊和宽泛的异常捕获可能会让程序免于崩溃,但也可能会带来更大的麻烦
- 异常捕获贵在精确,只捕获可能抛出异常的语句,只捕获可能的异常类型
- 有时候,让程序提早崩溃未必是什么坏事
- 完全忽略异常是风险非常高的行为,大多数情况下,至少记录一条错误日志
(4)当你抛出异常时: - 保证模块内抛出的异常与模块自身的抽象级别一致 - 如果异常的抽象级别过高,把它替换为更低级的新异常 - 如果异常的抽象级别过低,把它包装成更高级的异常,然后重新抛出 - 不要让调用方用字符串匹配来判断异常种类,尽量提供可区分的异常