Python 中更优雅的异常处理方案
实例引入
比如写 Python 的时候,举个最简单的算术运算和文件写入的例子,代码如下:
def process(num1,num2,file):
result = num1 / num2
with open(file,'w',encoding='utf-8') as f:
f.write(str(result))
这里我们定义了一个 process 方法,接收三个参数,前两个参数是 num1 和 num2,第三个参数是 file。这个方法会首先将 num1 除以 num2,然后把除法的结果写入到 file 文件里面。
就这么一个简单的操作,但是这个实现真的是漏洞百出:
- 没有判断 num1、num2 的类型,如果不是数字类型,那会抛出 TypeError。
- 没有判断 num2 是不是 0,如果是 0,那么会抛出 ZeroDivisionError。
- 没有判断文件路径是否存在,如果是子文件夹下的路径,文件夹不存在的话,会抛出 FileNotFoundError。
一些异常测试用例如下:
process(1, 2, 'result/result.txt')
process(1, 0, 'result.txt')
process(1, [2], 'result.txt')
如果把这几类的错误都进行异常处理的话,会写成什么样子呢?
def process_try(num1,num2,file):
try:
result = num1 / num2
with open(file,'w',encoding='utf-8') as f:
f.write(str(result))
except ZeroDivisionError:
print(f'{num2} can not be zero')
except FileNotFoundError:
print(f'file {file} not found')
except Exception as e:
print(f'except, {e.args}')
这时候我们观察到了什么问题?
- 代码一下子臃肿了起来,这里的异常处理都没有实现,仅仅是 print 了一些错误信息,但现在可以看到我们的异常处理代码可能比主逻辑还要多。
- 主逻辑代码整块被硬生生地缩进进去了,如果主逻辑代码比较多的话,那么会有大片大片的缩进。
- 如果再有相同的逻辑的代码,难道要再写一次 try except 这一坨代码吗?
可能很多人都在面临这样的困扰,觉得代码很难看但又不知道怎么修改。
当然上面的代码写法本身就不好,有几种改善的方案:
- 本身这个场景不需要这么多异常处理,使用判断条件把一些意外情况处理掉就好。
- 异常处理本身就不应该这么写,每个功能区域应该和异常处理单独分开,另外各个逻辑模块建议再分方法解耦。
- 使用 retrying 模块检测异常并进行重试。
- 使用上下文管理器 raise_api_error 来声明异常处理。
但上面的一些解决方案其实还不能彻底解决代码复用和美观上的问题,比如某一类的异常处理我就统一在一个地方处理,另外我的任何代码都不想因为异常处理产生缩进。
下面我们来了解一个库,叫做 Merry。
Merry
Merry,这个库是 Python 的一个第三方库,非常小巧,它实现了几个装饰器。通过使用 Merry 我们可以把异常检查和异常处理的代码分开,并可以通过装饰器来定义异常检查和处理的逻辑。
GitHub 地址:https://github.com/miguelgrinberg/merry,这个库的安装非常简单,使用 pip3 安装即可:
pip install merry
官方案例
入门
以这个函数为例,该函数嵌入了错误处理代码:
def write_to_file(filename, data):
try:
with open(filename, 'w') as f:
f.write(data)
except IOError:
print('Error: can't write to file')
except Exception as e:
print('Unexpected error: ' + str(e))
write_to_file('some_file', 'some_data')
即使使用这个简单的示例,您也可以看到try / except块强制执行的缩进如何使代码更难阅读和可视化。
Merry允许您将异常处理程序移至外部函数,以使它们不会干扰应用程序逻辑:
from merry import Merry
merry = Merry()
@merry._try
def write_to_file(filename, data):
with open(filename, 'w') as f:
f.write(data)
@merry._except(IOError)
def ioerror():
print('Error: can't write to file')
@merry._except(Exception)
def catch_all(e):
print('Unexpected error: ' + str(e)
write_to_file('some_file', 'some_data')
尽管在此示例中,使用merry后有更多的代码行,但主要好处是该write_to_file 函数中的应用程序逻辑现在完全清除了try / except语句。异常处理程序成为辅助功能,甚至可以移至单独的模块,以使它们完全不受干扰。
访问异常对象
装饰的异常处理程序可以选择接受一个参数。如果包含此参数,则merry发送异常对象。
@merry._except(Exception)
def catch_all(e):
print('Unexpected error: ' + str(e)
else 和 finally 语法
对于需要更复杂的try / except块的情况,还可以使用装饰器else和finally:
@merry._else
def else_clause():
print('No exceptions where raised!')
@merry._finally
def finally_clause():
print('Clean up time!')
Merry详情
使用Merry处理开始的案例
from merry import Merry
merry = Merry()
merry.logger.disabled = True
@merry._try
def process_merry(num1,num2,file):
result = num1 / num2
with open(file,'w',encoding='utf-8') as f:
f.write(str(result))
@merry._except(ZeroDivisionError)
def process_zero_division_error(e):
print('zero_division_error',e)
@merry._except(FileNotFoundError)
def process_file_not_found_error(e):
print('process_file_not_found_error', e)
@merry._except(Exception)
def process_exception(e):
print('process_exception', type(e), e)
process_try(1, 2, 'result/result.txt')
process_try(1, 0, 'result.txt')
process_try(1, [2], 'result.txt')
代码变得更优雅了
网友评论