异常与错误处理高级用法
异常与错误处理高级用法
1.概述
这篇文章介绍如何优雅的使用异常处理好程序的错误、用更少的代码、清晰的代码,写出更健壮的程序。
2.异常高级用法
2.1.两种编程风格处理错误
使用Python处理错误有两种风格,下面介绍下这两种风格。
- LBYL(look before you leap):LBYL常备翻译成“三思而后行”,通俗讲就是在执行一个可能会出错的操作时,先做一些关键条件判断,仅当条件满足时才进行操作。
LBYL是一种本能的思考结果,他的逻辑就像“如果天气预报说今天会下雨,那么我就不出门” - EAFP(easier to ask for forgiveness than permission),可直译为“获取原谅比许可简单”。 是一种和三思而后行截然不同的编程风格,它不做任何事前检查,直接执行操作,但在外层用try来捕获异常。
这种做法类似于“出门前不看天气预报,如果下雨了就回家吃感冒药”
在python社区更偏向于使用EAFP编程风格,它的代码通常更精简。因为它不需要开发者用分支覆盖各种可能出错的情况,只需要捕获可能发生的异常情况即可。
EAFP性能更好,它直奔主要代码,省去了各种条件判断。
1.三思而后行编程风格
def incr_by_one(value):
'''
输入的参数加1返回新值
:param value:
:return:
'''
if isinstance(value, int):
return value + 1
elif isinstance(value, str) and value.isdigit():
return int(value) + 1
else:
print(f'Unable to perform incr for value: {value}')
1.仅当类型是int类型时才执行加法操作
2.判断仅当类型时str,同时满足.isdigit() 方法时才进行操作。
这几行代码看似简单,其实代表了LBYL编程风格。
2.获取原谅比许可简单编程风格
def incr_by_try(value):
'''
输入的参数加1返回新值
:param value:
:return:
'''
try:
return int(value) + 1
except (KeyError, ValueError):
print(f'Unable to perform incr for value: {value}')
2.2.把更精确的except语句放在前面
python内置异常类之间存在许多继承关系,举个简单的依赖关系。
BaseException —> Exception—> LookupError—> KeyError
如果一个try代码块里包含多条except,异常匹配会按照从上而下的顺序执行。假如把一个父类异常放在前面,就会导致子类异常永远不会执行。
这个示例中KeyError异常永远不会被执行,要修复这个问题就要调整except的顺序。
def incr_by_key(d, key):
try:
d[key] + 1
except Exception as e:
print(f'Unknown error: {e}')
except KeyError:
print(f'key {key} does not exists')
2.3.try 搭配 else分支增强代码能力
用try捕获异常时,有时程序需要在一切操作没有异常后执行某个操作。为了做到这一点我们需要创建一个变量来做标记实现这个功能,如下示例。
只有当sync_profile()执行成功时,才继续调用send_notification()发送消息通知。为此我们定义一个额外变量syn_succeeded来作为标记。
def send_mess():
syn_succeeded = False
try:
sync_profile(user.profile)
syn_succeeded = True
except Exception as e:
print(f'Unknown error: {e}')
if syn_succeeded:
send_notification(user, 'profile sync succeeded')
如果使用try搭配else分支,代码可以变得更简单。
异常捕获语句里的else表示:仅当try语句块里没有抛出任何异常时,才执行else分支下的内容,效果就像在try最后增加一个标记变量一样。
def sen_mess_try_else():
try:
sync_profile(user.profile)
syn_succeeded = True
except Exception as e:
print(f'Unknown error: {e}')
else:
send_notification(user, 'profile sync succeeded')
2.4.使用空raise语句
在处理异常时,有时我们可能仅仅想记录下某个异常,然后把它重新抛出,交由上层处理。这是使用不带任何参数的raise语句可以派上用场。
当一个空raise出现在except时,他会原封不动的重新抛出当前异常,因此print语句不会执行。
def incr_by_raise(value):
try:
return value + 1
except TypeError:
print(f'key {value} does not exists')
raise
incr_by_raise('dd')
2.5.抛出异常而不是返回错误
python函数支持一次返回多个值,当我们表名函数执行出错时,可以让它同时返回结果与错误信息。
下面是create_item()函数就利用了这个特性,在这段代码里,create_item()函数的功能是创建新的Item对象。当调用create_item()函数,如果执行失败函数会把错误信息放到第二个结果中返回。而当函数执行成功时,为了保持返回值统一,函数同样返回错误原因,只是内容为空字符串。
这种做法看上去很自然,但在python世界里,返回错误并非解决此类问题的最佳办法。这是因为这种做法会增加调用方处理错误的成本
MAX_LENGTH_OF_NAME = 10
MAX_ITEMS_QUOTA = 5
def cerate_item(name):
'''
接收名称,创建item对象
:param name:
:return: 返回 结果,错误信息。如果执行成功返回错误信息为空
'''
if len(name) > MAX_LENGTH_OF_NAME:
return None, 'name of item is too long'
if len(get_current_items()) > MAX_ITEMS_QUOTA:
return None, 'item is full'
# 当执行成功后返回结果和空错误信息
return Item(name), ''
python有完善的异常处理机制,在某种程度上鼓励我们使用异常,所以用异常来处理错误才是更地道的做法。
通过引入自定义异常类,上面的代码可以改成下面的样子
MAX_LENGTH_OF_NAME = 10
MAX_ITEMS_QUOTA = 5
Item = []
def get_current_items():
return len(Item)
# 创建自定义异常类
class CreateItemError(Exception):
pass
def create_item_except(name):
if len(name) > MAX_LENGTH_OF_NAME:
# 向上抛出异常信息
raise CreateItemError('name of item is too long')
if len(get_current_items()) > MAX_ITEMS_QUOTA:
raise CreateItemError('item is full')
return Item(name), ''
def create_from_input():
name = input()
try:
item = create_item_except(name)
except CreateItemError as e:
print(f'create item failed: {e}')
else:
print(f'item<{name}> created')
create_from_input()
#运行结果
create item failed: name of item is too long
用抛出异常代替返回错误后,整个代码结构看上去变化不大但细节上改变非常多。
- 新函数拥有更稳定的返回值类型,他永远只会返回Item类型或是抛出异常。
- 不同于返回值,异常在被捕获前会不断往调用栈上层汇报。因此create_item()的直接调用方可以完全不用处理CreateItemError,而交由更上层处理。异常的这个特点给了我们更多灵活性,但同时也带来了风险。假如程序缺少一个顶级的统一异常处理逻辑,那么某个被所有人忽略的异常可能会层层上报,最终弄垮整个程序。
- 虽然我们鼓励使用异常,但异常总是不可避免的然人感到“惊讶”所以最好在函数文档里说明可能抛出的异常类型。