简介说明
异常
- 语法或逻辑错误
- 因为程序出现了错误而在正常控制流以外采取的行为
- 引起异常发生的错误
- 检测(和采取可能的措施)阶段
基础使用
try..except..else..finally
1 | import traceback |
Raise 语句
1 | # raise 语句允许程序员强制发生指定的异常 |
用户自定义异常
1 | # 异常通常应该直接或间接地从 Exception 类派生 |
定义清理操作 finally 语句
try 的可选子句 finally
用于定义必须在所有情况下执行的清理操作
1 | try: |
更复杂的异常情况(简单来说 finally 语句无论如何都会执行)
- try 子句发生异常时,没有被 except 捕获,会在 finally 执行完成后,重新引发异常
- 异常如果在 except 或 else 内部产生,会在 finally 执行完成后,重新引发异常
- 如果在执行 try 语句时遇到一个 break, continue 或 return 语句,则 finally 子句将在执行 break, continue 或 return 语句之前被执行
- 如果 finally 子句中包含一个 return 语句,则返回值将来自 finally 子句的某个 return 语句的返回值,而非来自 try 子句的 return 语句的返回值。
预定义的清理操作
Python 本身提供了很多语法范式简化了异常处理,例如:
- for 语句利用 Stoplteration 异常来结束循环的
- with 语句在打开文件后会在操作结束后(无论是否正常结束)会自动关闭文件句柄
- 使用 getattr() 函数获取对象中的不确定属性
以上这些都是 Python 自身封装好的语法范式,在处理这些事件的时候应避免使用 try/except/finally 的思维来处理。
小结
- except 语句不是必须的,finally 语句也不是必须的,但是二者必须要有一个,否则就没有 try 的意义了。
- except 语句可以有多个,Python 会按 except 语句的顺序依次匹配你指定的异常,如果异常已经处理就不会再进入后面的 except 语句。
- except 语句可以以元组形式同时指定多个异常,参见实例代码。
- except 语句后面如果不指定异常类型,则默认捕获所有异常,你可以通过 logging 或者 sys 模块获取当前异常。
- 如果要捕获异常后要重复抛出,请使用 raise,后面不要带任何参数或信息。
- 不建议捕获并抛出同一个异常,请考虑重构你的代码。
- 不建议在不清楚逻辑的情况下捕获所有异常,有可能你隐藏了很严重的问题。
- 尽量使用内置的异常处理语句来 替换 try/except 语句,比如 with 语句,getattr()方法
异常层级分析
异常层级
- BaseException
- SystemExit
- KeyboardInterrupt
- GeneratorExit
- Exception
- ….
1 | try: |
详细异常层级分析
1 | BaseException |
异常使用技巧
传递异常
有时我们会在捕捉到一个异常后重新引发它(传递异常),实现起来很简单,使用不带参数的 raise
语句即可,例如
1 | def f1(): |
只做精确的异常捕获
在 Python 中使用异常捕获时应捕获尽可能精确的异常类型,而不是模糊的 Exception。
别让异常破坏代码抽象分层的一致性
很多场景下我们会对异常类进行包装,方便在产生已知异常时自定义错误信息,这样做能大大提高后续的编码效率,但在使用时如果没有做好分层处理很容易击穿代码的抽象分层逻辑,具体案例请参考 Python 工匠: 异常处理的三个好习惯。
为了避免因为使用错误的异常处理方式导致代码的抽象分层逻辑被打破:
- 让模块只调用与当前抽象层级一致的异常类,既不能高于当前抽象层级,也不能低于当前抽象层级
- 在需要跨层级调用异常类时应通过异常包装与转换的方法进行,而不是直接跨层级调用异常类
异常处理不应该喧宾夺主
异常处理逻辑太多,以至于扰乱了代码核心逻辑
最佳实践
最佳实践不限于编程语言,只是一些规则和填坑后的收获。
- 只处理你知道的异常,避免捕获所有异常然后吞掉它们。
- 抛出的异常应该说明原因,有时候你知道异常类型也猜不出所以然的。
- 避免在 catch 语句块中干一些没意义的事情。
- 不要使用异常来控制流程,那样你的程序会无比难懂和难维护。
- 如果有需要,切记使用 finally 来释放资源。
- 如果有需要,请不要忘记在处理异常后做清理工作或者回滚操作。