Fork me on GitHub

Python系列文章-异常处理总结

目录

  • 背景

  • 第一部分 内置异常

  • 第二部分 异常处理

  • 第三部分 产生异常

  • 第四部分 自定义异常

  • 第五部分 traceback模块

  • 第六部分 cgitb包

  • 第七部分 最佳实践

  • 第八部分 补遗

  • 参考文献及资料

背景

和宇宙空间和时间的无限相比,人类的历史和现有活动只是这个空间和时间上有限抽样。我们无法穷尽所有的可能性,所以新增的事情(错误或异常)是客观存在的。

Python 中有两种错误:语法错误和异常( syntax errorsexceptions )。

语法错误,也被称作解析错误。

异常。即使一条语句或表达式在语法上是正确的,当试图执行它时也可能会引发错误。运行期检测到的错误称为 异常,并且程序不会无条件的崩溃

案例环境:Cpython 3.8.8

第一部分 内置异常

Python中一切皆为对象,异常也是。Python3builtins.py内置模块中定义了64个异常类,类继承关系如下。在Python3中所有异常类本质都是继承BaseException基类。

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
BaseException # 异常基类
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception # 常规错误的基类。所有内置的非系统退出类异常都继承此类,用户自定义异常也应当继承此类
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError # 此基类用于派生针对各种算术类错误而引发的内置异常
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError # 与缓冲区相关的操作无法执行时将被引发
+-- EOFError
+-- ImportError # Python导入失败错误
| +-- ModuleNotFoundError # Python模块不存在错误
+-- LookupError # 此基类用于派生当映射或序列所使用的键或索引无效时引发的异常
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError # 操作系统错误
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning # 警告类异常
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning

第二部分 异常处理:try-except-else-finally

2.1 语法

大部分语言中都有对异常处理的语法,例如Java中使用 try…catch 语句来捕获代码中的异常,而在Python中使用try…except。原理就是通过监视try语句中的错误,从而让except语句捕获异常信息并进行处理。基本语法结构为:

1
2
3
4
5
6
7
8
try:
# 代码段1
except exception name:
# 代码段2
else:
# 代码段3
finally:
# 代码段4
  • try:正常情况下,程序计划执行的语句。
  • except:程序异常是执行的语句。注意这里的exception name可以是多个。
  • else:程序无异常即try段代码正常执行后会执行该语句。
  • finally:不管有没有异常,都会执行的语句。

另外exception name是异常名。

  1. except语句不是必须的,finally语句也不是必须的,但是二者必须要有一个,否则就没有try的意义了。
  2. except语句可以有多个,Python会按except语句的顺序依次匹配你指定的异常,如果异常已经处理就不会再进入后面的except语句。
  3. except语句可以以元组形式同时指定多个异常,参见实例代码。
  4. except语句后面如果不指定异常类型,则默认捕获所有异常,你可以通过logging或者sys模块获取当前异常。

2.2 案例

1
2
3
4
5
6
7
8
9
10
f = open("test.txt", "r")

try:
f.write("写入内容")
except IOError as e:
print(e)
else:
print("写入文件成功")
finally:
f.close()
  • 文件以read模式打开,显然是无法写入的,except捕获IOError异常回显:not writable。但是程序能正常执行完成(exit code 0)
  • 如果我们使用write模式打开,就能顺利执行完
  • 另外如果文件不存在,程序不能正常执行完成( exit code 1,非正常退出),中断显示异常:FileNotFoundError。注意这个报错其实是语句f = open("test.txt", "r")的报错。

另外语法中except可以是多个分别定义,Python编译器会顺序处理捕获的异常。

第三部分 产生异常:raise和assert

3.1 raise

在调用Python第三方包或者内置包的时候,即使我们没有去捕获异常,通常遇到异常也会捕获。这就是因为第三方包开发者做好了严谨的异常生产和捕获。例如下面的例子我们直接使用raise方法产生一个IOError类型异常,然后被except捕获并处理。语法:

1
raise [exceptionName [(reason)]]

等同于C#和Java中的throw

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
import sys
import traceback

def raise_test():
raise IOError("异常生成测试")
try:
raise_test()
print("test")
except IOError as e:
print(e)
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_tb(exc_traceback)

finally:
pass

"""
异常生成测试
File "D:/git/Python/bestPractice/exceptionExample/example6.py", line 10, in <module>
raise_test()
File "D:/git/Python/bestPractice/exceptionExample/example6.py", line 6, in raise_test
raise IOError("异常生成测试")

Process finished with exit code 0
"""
1
2
3
4
5
6
7
8
9
10
11
12
>>> raise
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: No active exception to reraise
>>> raise IOError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError
>>> raise IOError("write error")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError: write error

3.2 assert 断言

Python中assert用于判断一个表达式(expression),在表达式条件为 false 的时候触发异常。

语法如下:

1
assert expression

也等价于:

1
2
if not expression:
raise AssertionError

看一个例子:

1
2
3
4
5
6
7
8
9
>>> assert 1/0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> assert 1/1
>>> assert 1==False
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError

第四部分 自定义异常

案例(requests)

除了系统内置的异常类,用户还可以自己定义应用相关的异常类。例如requests包在requests.exceptions中定义自身异常类如下:

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
IOError
+-- RequestException # 处理不确定的异常请求
+-- HTTPError # HTTP错误
+-- ConnectionError # 连接错误
| +-- ProxyError # 代理错误
| +-- SSLError # SSL错误
| +-- ConnectTimeout(+-- Timeout) # (双重继承,下同)尝试连接到远程服务器时请求超时,产生此错误的请求可以安全地重试。
+-- Timeout # 请求超时
| +-- ReadTimeout # 服务器未在指定的时间内发送任何数据
+-- URLRequired # 发出请求需要有效的URL
+-- TooManyRedirects # 重定向太多
+-- MissingSchema(+-- ValueError) # 缺少URL架构(例如http或https)
+-- InvalidSchema(+-- ValueError) # 无效的架构,有效架构请参见defaults.py
+-- InvalidURL(+-- ValueError) # 无效的URL
| +-- InvalidProxyURL # 无效的代理URL
+-- InvalidHeader(+-- ValueError) # 无效的Header
+-- ChunkedEncodingError # 服务器声明了chunked编码但发送了一个无效的chunk
+-- ContentDecodingError(+-- BaseHTTPError) # 无法解码响应内容
+-- StreamConsumedError(+-- TypeError) # 此响应的内容已被使用
+-- RetryError # 自定义重试逻辑失败
+-- UnrewindableBodyError # 尝试倒回正文时,请求遇到错误
+-- FileModeWarning(+-- DeprecationWarning) # 文件以文本模式打开,但Requests确定其二进制长度
+-- RequestsDependencyWarning # 导入的依赖项与预期的版本范围不匹配

Warning
+-- RequestsWarning # 请求的基本警告

[‘BaseHTTPError’, ‘ChunkedEncodingError’, ‘CompatJSONDecodeError’, ‘ConnectTimeout’, ‘ConnectionError’, ‘ContentDecodingError’, ‘FileModeWarning’, ‘HTTPError’, ‘InvalidHeader’, ‘InvalidJSONError’, ‘InvalidProxyURL’, ‘InvalidSchema’, ‘InvalidURL’, ‘JSONDecodeError’, ‘MissingSchema’, ‘ProxyError’, ‘ReadTimeout’, ‘RequestException’, ‘RequestsDependencyWarning’, ‘RequestsWarning’, ‘RetryError’, ‘SSLError’, ‘StreamConsumedError’, ‘Timeout’, ‘TooManyRedirects’, ‘URLRequired’, ‘UnrewindableBodyError’, ‘builtins‘, ‘cached‘, ‘doc‘, ‘file‘, ‘loader‘, ‘name‘, ‘package‘, ‘spec‘]

如何定义一个异常类

此外,你也可以通过创建一个新的异常类拥有自己的异常,异常应该是通过直接或间接的方式继承(直接或者间接)自Exception类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SelfExceptionError(Exception):
def __init__(self, msg):
self.msg = msg

def __str__(self):
return self.msg

try:
raise SelfExceptionError('用户自定义异常')
except SelfExceptionError as e:
print('user self defined exception occurred', e.msg)

# 执行回显:
# user self defined exception occurred 用户自定义异常
  • 自定义类内部实现了 __init__() 方法和 __str__() 方法
  • 由于大多数 Python 内置异常的名字都以 “Error” 结尾,所以实际命名时尽量跟标准的异常命名一样

第五部分 traceback模块查看异常

https://www.jianshu.com/p/a8cb5375171a

5.1 案例

在程序出现异常时候,如果只是打印异常(日志文件或回显),通常信息比较少,例如前面写文件的例子,只是返回:not writable。为了快速定位问题,需要知道是哪个文件、哪个模块以及哪一行出错。

Python程序的traceback信息均来源于一个叫做traceback object的对象,而这个traceback object通过函数sys.exc_info()来获取。我们先看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sys
import traceback

f = open("test.txt", "r")

try:
f.write("写入内容")
except IOError as e:
print(e)
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_tb(exc_traceback)
else:
print("写入文件成功")
finally:
f.close()

程序执行正常退出,回显如下。

1
2
3
4
not writable
File "D:/git/Python/example4.py", line 7, in <module>
f.write("写入内容")
Process finished with exit code 0

从回显信息中,有执行模块example4.py和异常发生的代码行信息:line 7。通过以上示例可以看出,sys.exc_info()获取了当前处理的exception的相关信息,并返回一个元组(对象名自定义)。

  • exc_type,异常的类型(示例是NameError类型);
  • exc_value, 异常的value值;
  • exc_traceback,traceback object;

5.2 Traceback 模块

模块提供了一个标准接口,用于提取,格式和打印Python程序的堆栈痕迹。 它完全模仿了Python解释器在打印堆栈跟踪时的行为。 当您想在程序控制下打印堆栈迹线时,这非常有用,例如在解释器周围的“包装器”中。
Python中的traceback信息均来源于一个叫做traceback object的对象,而这个traceback object通常是通过函数sys.exc_info()来获取的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import traceback
import sys

f = open("test.txt", "r")

try:
f.write("写入内容")
except IOError as e:
print(e)
exc_type, exc_value, exc_traceback = sys.exc_info()
print("exc_type:", exc_type)
print("exc_value:", exc_value)
print("exc_traceback:", exc_traceback)
traceback.print_tb(exc_traceback)

else:
print("写入文件成功")
finally:
f.close()

程序运行:

1
2
3
4
5
6
7
8
not writable
exc_type: <class 'io.UnsupportedOperation'>
exc_value: not writable
exc_traceback: <traceback object at 0x000000000A7BBE40>
File "D:/git/Python/example3.py", line 9, in <module>
f.write("写入内容")

Process finished with exit code 0

sys.exc_info()获取了当前处理的exception的相关信息,并返回一个元组,元组的第一个数据是异常的类型,第二个返回值是异常的value值,第三个就是我们要的traceback objec

5.3 Traceback中常用函数

第六部分 cgitb包

cgitb模块为Python脚本提供了一个特殊的异常管理器。名字有点误导人,它最初设计是为了以HTML格式展示cgi脚本的大量异常信息。后来,他扩展为也可以展示纯文本信息。该模块激活后,如果发生了未捕获的异常,将会展示格式化的输出报告。该报告包括源代码每一层的回溯,以及当前执行程序的参数和局部变量。以及,你可以选择将这些信息存到一个文件里,而不是发送到浏览器。

6.1 案例

1
2
3
4
5
6
import cgitb
cgitb.enable(format='text')

f = open("test.txt", "r")
f.write("写入内容")
f.close()

6.2 内部方法和函数

两个函数:

1
cgitb.encable(display=1, logdir=None, context=5, format="html")

display 1,发送至浏览器;0, 不发送
logdir 如果有的话,写到该目录下
context 显示错误代码周围的代码行数
format 是否显示为HTML,除了’html’之外的所有值,都会显示为纯文本

1
cgitb.handle(info=None)

如果你想用cgitb处理异常,你可以调用这个函数。
info 应当是含有异常类型、异常值和traceback对象的三元组,——如同sys.exc_info()返回的那样。如果不提供info,则从sys.exc_info中获取。

第七部分 最佳实践

  1. 只处理你知道的异常,避免捕获所有异常然后吞掉它们。
  2. 抛出的异常应该说明原因,有时候你知道异常类型也猜不出所以然。
  3. 避免在catch语句块中干一些没意义的事情,捕获异常也是需要成本的。
  4. 不要使用异常来控制流程,那样你的程序会无比难懂和难维护。
  5. 如果有需要,切记使用finally来释放资源。
  6. 如果有需要,请不要忘记在处理异常后做清理工作或者回滚操作。

  7. 不建议捕获并抛出同一个异常,请考虑重构你的代码。

  8. 不建议在不清楚逻辑的情况下捕获所有异常,有可能你隐藏了很严重的问题。
  9. 尽量使用内置的异常处理语句来替换try/except语句,比如with语句,getattr()方法。

第八部分 补遗

8.1 except Exception语法

在查看Python源码的时候经常看到下面2种写法。

1
2
3
4
5
6
try:
# 执行逻辑
except NameError as e:
pass
except KeyError, e:
pass

在语法上,Python2可以使用2种语法均可。但是Python3中只能使用第一种,即:except NameError as e。为了统一,建议使用第一种写法。

参考文献及资料

1、Understanding the Python Traceback:https://realpython.com/python-traceback/

0%