背景

在用异常的时候,会有这么几个疑问 * 异常是直接raise,让程序退出,还是捕获记录到日志中后继续运行 * 异常的代码影响到了代码的可读性, 有没有更合适的解决方式?

异常的使用场景

详见if-else与try-catch初理解

面向切面编程(AOP)[^6]

Todo:异常的代码可读性

像是如果一个业务逻辑里要执行多种封装的接口调用

1
2
3
4
5
6
7
8
9
try:
# code 1
except CustomException:
pass

try:
# code 2
except CustomException2:
pass

这种应该是有些类似装饰器,阅读代码中

捕获异常记录到日志里

一直有一个疑问,究竟是捕获异常,然后按照约定的接口输出错误信息,还是直接抛出,让程序直接退出呢?

根据目前写的比较多的几种场景的程序来看 * 守护程序, 进程间通信运作 * 接口程序,执行完即退出

无论是上面哪种场景,按照接口交互的开发标准来说,已知的异常都应该按照拟定的接口格式,错误时输出约定的错误码及信息,而不应该直接将exception直接抛出, 这样对其他调用解析你的接口的人来说,会产生未知错误.

只不过,这里的封装只在最上层封装, 然后在内部依旧可以使用raise,便于直接从底层快速将问题抛到上层的try...except处.(这里主要是相比return, 能快速退出多层接口调用)

注意点

PS. 虽然直接把异常抛到上层是很便捷的做法,但是对于一个公有库来说,最好还是在自己的抽象层定义自己的异常, 一个层次只处理他调用的层次的异常,而不是直接向上暴露底层异常.

当然,这里封装的异常同样也是已知异常,对于未知异常,只能尽可能靠单元测试来发现并捕捉,不然就只能抛到最上层捕获记录到日志里,修缺陷时再进行补全捕获了.

单元测试不可疏忽

而对于未知的错误, 肯定就是直接抛出了,但是这部分是需要在单元测试中尽可能覆盖完全来避免出现的.

样例

那么,怎么才能把traceback栈信息记录到日志里呢?

目前我用的比较多的是python3.6, 这个版本的logging.log提供了exc_info的选项

1
2
3
4
5
6
try:
# coode in here
except Exception as e:
logging.error(e, exc_info=True)
# or
logging.exception(e)

接口拟定正常

直接抛出Exception打断运行

正常来说,我们提供接口时,会约定一套返回值及内容格式,以及这个接口执行出现问题时的反应,如抛出什么异常,来让调用者捕获.这是Python的推荐做法.

exception作为返回值

但是我看到[^4]这样一篇文章,里面提到了将Exception作为返回值,并且还存在一套针对这种用法的比较完备的库returns. 就仿佛静态类型里的非0

单纯就这个做法来说,可能不是比较Pythonic的,但是按照readme来看,比较贴合目前python对静态类型的青睐?因为可以完全利用上类型标注的检查效果. 对于遗漏的异常返回值检查,可以做到很好的效果. 而且这样,又不会丢失Exception饱含的错误信息.

但是,又有一个问题,程序调用栈可能就会被破坏了,就像下面说的.

我调用一个库要一个int类型的结果,结果这个库里的接口在失败时并不会直接抛出,打断运行,而是返回一个Exception,我必须增加额外的判断检查,才能使用,否则一定会让调用者出错,这样其实也是违背了静态类型的接口设计的吧. 谁的问题应该由谁抛出更合适吧.

不捕获异常,上层也不做捕获,直接抛出

但是又看到一种说法[^5],有人说是由于python底层针对各种语句,存在各式各样的异常,基本不可能捕获,这个语言就是这么脆弱,捕获异常也只能做到非常有限的效果的话, 直接让程序出错直接退出就好了,如果这个场景没有考虑到的话.

但是按照我的理解, 如果底层的异常太多没处理好,其实就说明底层就开始有问题吧,应该一层层逐渐封装异常,最上层的使用不应该直接接触底层异常,而是下层逻辑的抽象异常才对. 这本就属于业务异常要考虑的逻辑.

这里python之父居然参与讨论了,回答了问题了.

Let me draw a line in the sand. The PEP will not support any form of exception checking. The only thing possibly under discussion here is whether there is some other use of stubs (maybe an IDE suggesting a raise or try/except) that might benefit from declaring exceptions. But so far everything brought up has just been about the relative advantages of checked exceptions, and on that issue is close. We won't do it.

The PEP doesn't mandate any particular behavior from a type checker, so I'm not prohibiting you from doing something you find useful. Whether it is actually useful may well depend on the codebase you are checking. I just don't want to have to put anything in the PEP that would seem to make checked exceptions part of the signature of a function. Maybe as a compromise we can just say in the PEP that a conformant type checker should not interpret the body of functions in stubs, and you can have a non-conformant option that interprets raise statements in stub function bodies.

PEP里是不是没有这样的讨论?

Never throw an exception of my own Always catch any possible exception that might be thrown by a library I’m using on the same line as it is thrown and deal with it immediately.

It's all well and good that exceptions are widely used in core Python constructs, but why is a different question.

Reference

  1. python - Log exception with traceback - Stack Overflow
  2. logging — Logging facility for Python — Python 3.8.3 documentation
  3. Python 工匠: 异常处理的三个好习惯 - 掘金
  4. Python exceptions considered an anti-pattern - DEV
  5. declaring exceptions in stubs · Issue #71 · python/typing
  6. Python小书4-统一异常处理 - 知乎