一瞥之见

0%

谈谈调用链中的异常处理问题

由于调用很多情况下都存在子调用、而子调用链上很可能某一环就抛出了异常,此文主要讲我是怎么看待和处理这种情况的

不好的做法

什么都不做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def c():
raise Exception('xxx')

def b():
# dong pre things
c()
# dong after things

def a():
b()


if __name__ == '__main__':
a()

"""
输出:
Traceback (most recent call last):
File "/.../tmp.py", line 14, in <module>
a()
File "/.../tmp.py", line 10, in a
b()
File "/.../tmp.py", line 6, in b
c()
File "/.../tmp.py", line 2, in c
raise Exception('xxx')
Exception: xxx
"""
1. 这样导致程序直接退出了、而得到的报错信息是程序报错的信息,这样是不能够提供给用户(非开发人员)的
2. 有可能在b环节里、调用c压根不是个重要的事情(假设是打日志到某日志收集平台)、那么由于c发生的异常导致整体让用户认为操作失败了是完全没有必要的
3. c真的应该raise Exception吗?这会导致所有其调用者都需要使用try ... catch ...来捕捉错误信息

所有子调用都统一返回result、error格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def c():
return None, Exception('xxx')

def b():
# dong pre things
c_res, err = c()
# 若我(b)认为是关键步骤、就直接返回 None,'从b的角度转化为可读性更高的报错信息'
# 否则继续往下走
# dong after things

return 'sth', None

def a():
b_resp, err = b()

if __name__ == '__main__':
a()
1. 优点:b能够处理上面提到的没必要因为不重要事情而退出的情况
2. 优点:a拿到b的可读性更好的报错信息,其可以根据自己对于b业务的重要性等判断来决定是否处理以及返回的错误信息(从a的角度理解是a最接近用户、最接近具体业务、其能返回可读性更高的信息、以及更能决定哪些步骤才是核心步骤; 从b的角度理解就是b只提供特定功能,此功能在调用b的不同调用者中的重要程度不同,对于b处理结果的看待方式应由调用者决定)
2. 缺点:从a的角度拿到的信息少了底层(c)的真实程序报错信息,不方便开发者来后续分析与处理

我希望做到的是若某次的调用链其中某一/几环出现了异常

  1. 尽量不raise Exception从而避免自己其实是个不重要的功能却导致整体的调用失败
  2. 最后能返回给用户可读性非常高的异常信息
  3. 能够提供给开发者掌握到各个调用的异常详情信息以方便排查/修复

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
"""
现在我的项目是将Ret实现到一个每个文件都应该导入的一个包里,并确保所有的返回都是Ret实例
这里为了方便写到一起
对比之前我习惯以result, error的返回方式,我觉得这种方式的优点会多不少(对于"核心思想")
"""
class Ret:
def __init__(self, result=None, error=[]):
self.result = result
self._err = [] if not error else [error] # always keep error as a list

def __setattr__(self, key, value):
if key == 'error':
self._err.append(value)
return
return super().__setattr__(key, value)

@property
def error(self):
if self.result or not self._err:
return None
return self._err.__str__()

def add_error(self, value):
self._err.append(value)

def __bool__(self):
return len(self._err) == 0

def __str__(self):
if self:
return f'SUC with result: {self.result}'
else:
return f'FAIL with error: {self.error}'


def c():
ret = Ret()
# 模拟处理有问题

ret.error = 'c failed'
return ret

def b():
# ...

ret = c()

if ret.error:
# 此处b更有决定权是否/如何处理c返回出错的情况
# 如果error是有效且不能继续下面的操作时,return ret即可
pass

# 这里假设b的处理也出错了
# 只需要继续对ret.error进行赋值即可会自动加到error_list中,使用负担小,直接
ret.error = 'b failed'

return ret

def a():
ret = b()
print(ret)
print(ret.error)
# 此处最高层应该更贴近业务,更能够决定如何处理此error


if __name__ == '__main__':
a()

"""
输出:
FAIL with error: ['c failed', 'b failed']
['c failed', 'b failed']
"""