python通用规范-5

文章目录

5.1 异常处理
5.1.1 异常捕获后要加 `finally`
5.1.2 异常捕获时需注明异常类型
5.1.3 不在`except`分支里面的`raise`都必须带异常
5.1.4 尽量用异常来表示特殊情况,而不要返回None
5.1.5不在`finally`中使用`return`或者`break`
5.1.6 禁止使用`except X, x`语法
5.2 异常恢复
5.3 断言
5.1 异常处理

5.1.1 异常捕获后要加 finally

使用try…except…结构对代码作保护时,需要在异常后使用finally…结构保证操作对象的释放
说明:
使用try…except…结构对代码作保护时,如果代码执行出现了异常,为了能够可靠地关闭操作对象,需要使用finally…结构确保释放操作对象。
示例:

handle = open(r"/tmp/sample_data.txt") # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
except UnicodeDecodeError as decode_error:
print(decode_error)
finally:
handle.close() # Always run after try:
1
2
3
4
5
6
7
5.1.2 异常捕获时需注明异常类型

不要使用except:语句来捕获所有异常

说明:
在异常这方面, Python非常宽容,except:语句真的会捕获包括Python语法错误在内的任何错误。使用except:很容易隐藏真正的bug,我们在使用try…except…结构对代码作保护时,应该明确期望处理的异常。 Exception类是大多数运行时异常的基类,一般也应当避免在except语句中使用。通常,try只应当包含必须要在当前位置处理异常
的语句,except只捕获必须处理的异常。比如对于打开文件的代码,try应当只包含open语句,except只捕获FileNotFoundError异常。对于其他预料外的异常,则让上层函数捕获,或者透传到程序外部来充分暴露问题。
# 错误示例:
# 如下代码可能抛出两种异常,使用"except:"语句进行统一处理时,如果是open执行异常,将在"except:"语句之后handle无效的情况下调用close,报错handle未定义。
try:
handle = open(r"/tmp/sample_data.txt") # May raise IOError
data = handle.read() # May raise UnicodeDecodeError
except:
handle.close()

# 正确示例:
try:
handle = open(r"/tmp/sample_data.txt") # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
except UnicodeDecodeError as decode_error:
print(decode_error)
finally:
handle.close()
except (FileNotFoundError, IOError) as file_open_except:
print(file_open_except)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
5.1.3 不在except分支里面的raise都必须带异常

说明:raise关键字单独使用只能出现在try-except语句中,重新抛出except抓住的异常。

# 错误示例:
>>> a = 1
>>> if a==1:
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: exceptions must be old-style classes or derived from BaseException, not
NoneType


# 正确示例1:raise一个Exception或自定义的Exception
>>> a = 1
>>> if a==1:
... raise Exception
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
Exception

# 正确示例2:在try-except语句中使用
>>> import sys
>>>
>>> try:
... f = open('myfile.txt')
... s = f.readline()
... i = int(s.strip())
... except IOError as e:
... print("I/O error({0}): {1}".format(e.errno, e.strerror))
... except ValueError:
... print("Could not convert data to an integer.")
... except:
... print("Unexpected error:", sys.exc_info()[0])
... raise

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
5.1.4 尽量用异常来表示特殊情况,而不要返回None

当我们在一个工具方法时,通常会返回None来表明特殊的意义,比如一个数除以另外一个数,如果被除数为零,那么就返回None来表明是没有结果的

def divide(a, b):
try:
return a/b
except ZeroDivisionError:
return None
result = divide(x, y)
if result is None:
print('Invalid inputs')
1
2
3
4
5
6
7
8
当分子为零是会返回什么?应该是零(如果分母不为零的话),上面的代码在if条件检查就会被忽略掉,if条件不仅仅只检查值为None,还要添加所有条件为False的情况了

x, y = 0, 5
result = divide(x, y)
if not result:
print('Invalid inputs') #This is wrong!
1
2
3
4
上面的情况是python编码过程中很常见,这也为什么方法的返回值为None是一种不可取的方式,这里有两种方法来避免上面的错误。

1.第一种方法是将返回值分割成一个tuple,第一部分表示操作是否成功,第二部分是实际的返回值(有点象go语言里的处理)

def divide(a, b):
try:
return True, a / b
except ZeroDivisionError:
return False, None
1
2
3
4
5
调用此方法时获取返回值并解开,检查第一部分来代替之前仅仅检查结果。

success, result = divide(x, y)
if not success:
print('Invalid inputs')
1
2
3
这种方式会带来另外一个问题,方法的调用者很容易忽略掉tuple的第一部分(通过在python里可以使用_来标识不使用的变量),这样的代码乍一看起来不错,但是实际上和直接返回None没什么两样。

_, result = divide(x, y)
if not result:
print('Invalid inputs')
1
2
3
2.接下来,另外一种方式,也是推荐的一种方式,就是触发异常来让调用者来处理,方法将触发ValueError来包装现有的ZeroDivisionError错误用来告诉方法调用者输入的参数是有误的。

def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
1
2
3
4
5
那么方法调用者必须要处理错误输入值而产生的异常(方法的注释应该注明异常情况)。同时也不用去检查返回值,因为当方法没有异常抛出时,返回值一定是对的,对于异常的处理也是很清晰。

x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
print('Invalid inputs')
else:
print('Result is %.1f' % result)
>>>
Result is 2.5
1
2
3
4
5
6
7
8
9
需要记住的:
(1)方法使用None作为特殊含义做为返回值是非常糟糕的编码方式,因为None和其它的返回值必须要添加额外的检查代码。
(2)触发异常来标示特殊情况,调用者会在捕获异常来处理。

5.1.5不在finally中使用return或者break

避免finally中可能发生的陷阱,不要在finally中使用return或者break语句
通常使用finally语句,表明要释放一些资源,这时候try和except还有else代码块都被执行过了,如果在执行它们的过程中有异常触发,且没有处理这个异常,那么异常会被暂存,当finally代码执行后,异常会重新触发,但是当finally代码块里有return或break语句时,这个暂存的异常就会丢弃:

def f():
try:
1/0
finally:
return 42
print(f())
1
2
3
4
5
6
上面的代码执行完后1/0产生的异常就会被忽略,最终输出42,因此在finally里出现return是不可取的。
当try块中return,break,continue执行时,finally块依然会被执行。

def foo():
try:
return 'try'
finally:
return 'finally'
>>> foo()
'finally'
1
2
3
4
5
6
7
最终方法的输出其实不是正确的结果,但出现这个问题的原因是错误使用了return和break语句。

5.1.6 禁止使用except X, x语法

禁止使用except X, x语法,应当使用except X as x

说明: except X, x语法只在2.X版本支持,3.X版本不支持,有兼容性问题。而且,except X, x写法容易和多异常捕获的元组(tuple)表达式混淆。因此应该统一用except X as x方式。
5.2 异常恢复

方法发生异常时要恢复到之前的对象状态
说明:当发生异常的时候,对象一般需要——如果是关键的安全对象则必须——维持其状态的一致性。常用的可用来维持对象状态一致性的手段包括:
1.输入校验(如校验方法的调用参数)
2.调整逻辑顺序,使可能发生异常的代码在对象被修改之前执行
3.当业务操作失败时,进行回滚
4.对一个临时的副本对象进行所需的操作,直到成功完成这些操作后,才把更新提交到原始的对象
5.避免需要去改变对象状态

# 错误示例:
PADDING = 2
MAX_DIMENSION = 10
class Dimensions:
def __init__(self, length, width, height):
self.length = length
self.width = width
self.height = height
def get_volume_package(self, weight):
self.length += PADDING
self.width += PADDING
self.height += PADDING
try:
self.validate(weight)
volume = self.length * self.width * self.height
self.length -= PADDING
self.width -= PADDING
self.height -= PADDING
return volume
except Exception , ex:
return -1
def validate(self, weight) :
# do some validation and may throw a exception
if weight>20:
raise Exception
pass
if __name__ == '__main__'
d = Dimensions(10, 10, 10)
print(d.getVolumePackage(21)) # Prints -1 (error)
print(d.getVolumePackage(19)) # Prints 2744 instead of 1728
# 在这个错误示例中,未有异常发生时,代码逻辑会恢复对象的原始状态。但是如果出现异常事件,则回滚代码不会被执行,从而导致后续的getVolumePackage()调用不会返回正确的结果。

# 正确示例1:回滚
except Exception as ex:
self.length -= PADDING
self.width -= PADDING
self.height -= PADDING
return -1
# 这个正确示例在getVolumePackage()方法的except块中加入了发生异常时恢复对象状态的代码。

# 正确示例2:finally子句
def getVolumePackage(self, weight):
self.length += PADDING
self.width += PADDING
self.height += PADDING
try:
self.validate(weight)
volume = self.length * self.width * self.height
return volume
except Exception as ex:
return -1
finally:
self.length -= PADDING
self.width -= PADDING
self.height -= PADDING
# 这个正确示例使用一个finally子句来执行回滚操作,以保证不管是否发生异常,都会进行回滚。

# 正确示例3:输入校验
def getVolumePackage(self, weight):
try:
self.validate(weight)
except Exception as ex:
return -1
self.length += PADDING
self.width += PADDING
self.height += PADDING
volume = self.length * self.width * self.height
self.length -= PADDING
self.width -= PADDING
self.height -= PADDING
return volume
# 这个正确示例在修改对象状态之前执行输入校验。注意,try代码块中只包含可能会抛出异常的代码,而其他代码都被移到try块之外。

# 正确示例4:未修改的对象
def getVolumePackage(self, weight):
try:
self.validate(weight)
except Exception as ex:
return -1
self.length += PADDING
self.width += PADDING
self.height += PADDING
volume = (self.length + PADDING) * (self.width + PADDING) * (self.height + PADDING)
return volume
# 这个正确示例避免了需要修改对象,使得对象状态不可能不一致,也因此没有必要进行回滚操作。相比之前的解决方案,更推荐使用这种方式。但是对于一些复杂的代码,这种方式可能无法实行。

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
74
75
76
77
78
79
80
81
82
83
84
85
86
5.3 断言

assert语句通常只在测试代码中使用,禁止在生产版本中包含assert功能
assert语句用来声明某个条件是真的。例如,如果你非常确信某个列表中至少有一个元素,而你想检验这一点,并且在它非真时触发一个异常,那么assert就是这种场景下的不二之选。当assert语句失败的时候,会触发AssertionError异常

>>> mylist = ['item']
>>> assert len(mylist) >= 1
>>> mylist.pop()
'item'
>>> assert len(mylist) >= 1
Traceback (most recent call last): File "<stdin>", line 1, in ? AssertionError
1
2
3
4
5
6
assert只应在研发过程中内部测试时使用,出现了AssertionError异常说明存在软件设计或者编码上的错误,应当修改软件予以解决。在对外发布的生产版本中禁止包含assert功能。
————————————————
版权声明:本文为CSDN博主「zhao12501」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhao12501/article/details/115456774

posted @ 2021-07-11 00:15  physique  阅读(198)  评论(0编辑  收藏  举报