最近学习 Scala 的异常处理模块时 Scala 模式匹配与异常处理,发现自己对于 Python 中的异常处理地非常不好并且进行 code review 时代码体验非常差,因此本文简单介绍下 Python 中的异常处理以及如何扩展其异常处理模块!

背景

为什么需要进行异常处理呢?例如解析器去执行程序并检测到了一个错误时,触发异常,异常触发后且没被处理的情况下,程序就在当前异常处终止,后面的代码不会运行,那么就这样崩溃的软件必然带给用户糟糕的体验。所以必须提供一种异常处理机制来增强你程序的健壮性与容错性。

python 详细的异常处理情况可以参考教程:https://www.runoob.com/python/python-exceptions.html

首先引入python中的常用操作作为示例来了解 python 中的异常,例如除法运算和文件读取操作,如下所示:

1
2
3
4
def example(num1, num2, path):
result = num1 / num2
with open(path, 'r') as file:
file.read()

上述代码定义的函数 example 中包含三个参数:num1,num2,path,函数内首先执行除法运算再执行文件读操作!如果没有发生异常时上述代码可以正确执行,但正常情况下都需要对操作可以发生的异常进行判断并对抛出的异常进行处理,这样才能增加代码的健壮性!

如果不添加异常处理模块,以上述函数 example 为例直接添加异常测试,那么可能出现以下异常情况:

1
2
# 除数为0那么会抛出 ZeroDivisionError 异常
example(1, 0, 'test.txt')
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-2-fcf03ade5770> in <module>
      1 # 除数为0那么会抛出 ZeroDivisionError 异常
----> 2 example(1, 0, 'test.txt')

<ipython-input-1-7f29291a2290> in example(num1, num2, path)
      1 def example(num1, num2, path):
----> 2     result = num1 / num2
      3     with open(path, 'r') as file:
      4         file.read()

ZeroDivisionError: division by zero
1
2
# 除法运算操作数的类型不为数字时那么会抛出 TypeError 异常
example(1, 'a', 'test.txt')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-3-12485c8f4513> in <module>
      1 # 除法运算操作数的类型不为数字时那么会抛出 TypeError 异常
----> 2 example(1, 'a', 'test.txt')

<ipython-input-1-7f29291a2290> in example(num1, num2, path)
      1 def example(num1, num2, path):
----> 2     result = num1 / num2
      3     with open(path, 'r') as file:
      4         file.read()

TypeError: unsupported operand type(s) for /: 'int' and 'str'
1
2
# 读文件时如果当前路径下不存在该文件则会抛出 FileNotFoundError 异常
example(1, 2, 'test.txt')
---------------------------------------------------------------------------

FileNotFoundError                         Traceback (most recent call last)

<ipython-input-4-84ce3176b749> in <module>
      1 # 读文件时如果当前路径下不存在该文件则会抛出 FileNotFoundError 异常
----> 2 example(1, 2, 'test.txt')

<ipython-input-1-7f29291a2290> in example(num1, num2, path)
      1 def example(num1, num2, path):
      2     result = num1 / num2
----> 3     with open(path, 'r') as file:
      4         file.read()

FileNotFoundError: [Errno 2] No such file or directory: 'test.txt'

如果不进行异常处理执行上述定义的 example 函数就可能抛出这么多异常并且在终端输出这么多错误信息,作为一个处女座的程序员简直是难以忍受啊!

因此,我们在函数中的运算操作中加上异常处理判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
def example(num1, num2, path):
try:
result = num1 / num2
with open(path, 'r') as file:
file.read()
except ZeroDivisionError:
print("num2 cannot be zero!")
except TypeError:
print("num1 and num2 should be number!")
except FileNotFoundError:
print(f"file {path} not found!")
except Exception as e:
print(f'exception information: {e.args}')

在函数中添加异常处理语句之后,再次测试异常情况:

1
2
3
example(1, 0, 'test.txt')
example(1, 'a', 'test.txt')
example(1, 2, 'test.txt')
num2 cannot be zero!
num1 and num2 should be number!
file test.txt not found!

终端输出这些异常信息就看起来舒服多了啊,而且异常抛出时后续的语句同样执行!

但是,上述异常处理语句同样存在问题:

  • 代码语句复杂,异常处理语句比操作运算都多!
  • 如果代码中包含相同的异常处理情况而try...except语句不可复用!
  • 逻辑代码模块与异常处理模块在同一区域略显复杂,如果在单独一个模块中专门用来异常处理?

针对上述两个问题,可以使用 python 第三方库 merry 来解决!

Merry 异常处理库

Merry 这第三方异常处理库的目的是将异常处理与业务逻辑分离,通过装饰器来实现异常检查和异常处理!

Merry 安装:pip install merry

在安装merry库之后,上述定义的 example 函数中异常处理可以重写为以下形式:

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
from merry import Merry

merry = Merry()
merry.logger.disabled = True

# _try 装饰器监听异常
@merry._try
def example(num1, num2, path):
result = num1 / num2
with open(path, 'r') as file:
file.read()

# _except 异常处理
@merry._except(ZeroDivisionError)
def process_zero_division_error(e):
print('zero_division_error', e)

@merry._except(TypeError)
def process_type_error(e):
print("type_error", e)

@merry._except(FileNotFoundError)
def process_file_not_found_error(e):
print('file_not_found_error', e)

@merry._except(Exception)
def process_exception(e):
print('exception', type(e), e)

上述代码中通过 Merry 库的_try_except 装饰器实现了异常的监听和处理,对于每一种异常情况都有其对应的处理方法,这样代码格式就好看多了啊!而且将异常处理方法和逻辑代码分离开不就可以重用了!

测试下异常处理的效果:

1
2
3
example(1, 0, 'test.txt')
example(1, 'a', 'test.txt')
example(1, 2, 'test.txt')
zero_division_error division by zero
type_error unsupported operand type(s) for /: 'int' and 'str'
file_not_found_error [Errno 2] No such file or directory: 'test.txt'

这样不就舒服多了啊,而且还可以对异常函数进行封装!

当然本文仅介绍了简单的用法 更多用法可以参考源码链接哦!传送门~

联系作者