tongsiying

阅读|运动|自律

0%

第19篇:异常处理

**学习目标**:

了解Python异常处理的常用知识,掌握异常处理的基本用法。

为增强程序的健壮性, 我们也需要考虑异常处理方面的内容。 例如, 在读取文件时需要考虑文件不存在、 文件格式不正确等异常情况。 这就是本节要介绍的异常处理。

一、第一个异常示例

在数学中, 任何整数都不能除以0, 如果在计算机程序中将整数除以0, 则会引发异常。

1
2
3
4
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
res = int(a) / int(b)
print(f'{a} / {b} = {res}')

由于0不能做除数,所以当我们将第二个数输入0的时候,程序就会报错

1
2
3
ZeroDivisionError                         Traceback (most recent call last)
...
ZeroDivisionError: division by zero

Traceback信息是异常堆栈信息,描述了程序运行的过程及引发异常的相关信息。通过分析异常堆栈信息,我们可以分析程序哪里出了问题。在Python中,异常类命名的主要后缀有Exception、Error和Warning,也有少数几个没有以这几个后缀命名,我们将它们统一翻译为异常。

二、Python内置异常

Python的异常处理能力是很强大的,它有很多内置异常,可向用户准确反馈出错信息。在Python中,异常也是对象,可对它进行操作。BaseException是所有内置异常的基类,但用户定义的类并不直接继承BaseException,所有的异常类都是从Exception继承,且都在exceptions模块中定义。Python自动将所有异常名称放在内建命名空间中,所以程序不必导入exceptions模块即可使用异常。一旦引发而且没有捕捉SystemExit异常,程序执行就会终止。如果交互式会话遇到一个未被捕捉的SystemExit异常,会话就会终止。内置异常类的层次结构如下:

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 # 用户中断执行(通常是输入^C)
+-- GeneratorExit # 生成器(generator)发生异常来通知退出
+-- Exception # 常规异常的基类
+-- StopIteration # 迭代器没有更多的值
+-- StopAsyncIteration # 必须通过异步迭代器对象的__anext__()方法引发以停止迭代
+-- ArithmeticError # 各种算术错误引发的内置异常的基类
| +-- FloatingPointError # 浮点计算错误
| +-- OverflowError # 数值运算结果太大无法表示
| +-- ZeroDivisionError # 除(或取模)零 (所有数据类型)
+-- AssertionError # 当assert语句失败时引发
+-- AttributeError # 属性引用或赋值失败
+-- BufferError # 无法执行与缓冲区相关的操作时引发
+-- EOFError # 当input()函数在没有读取任何数据的情况下达到文件结束条件(EOF)时引发
+-- ImportError # 导入模块/对象失败
| +-- ModuleNotFoundError # 无法找到模块或在在sys.modules中找到None
+-- LookupError # 映射或序列上使用的键或索引无效时引发的异常的基类
| +-- IndexError # 序列中没有此索引(index)
| +-- KeyError # 映射中没有这个键
+-- MemoryError # 内存溢出错误(对于Python 解释器不是致命的)
+-- NameError # 未声明/初始化对象 (没有属性)
| +-- UnboundLocalError # 访问未初始化的本地变量
+-- OSError # 操作系统错误,EnvironmentError,IOError,WindowsError,socket.error,select.error和mmap.error已合并到OSError中,构造函数可能返回子类
| +-- BlockingIOError # 操作将阻塞对象(e.g. socket)设置为非阻塞操作
| +-- ChildProcessError # 在子进程上的操作失败
| +-- ConnectionError # 与连接相关的异常的基类
| | +-- BrokenPipeError # 另一端关闭时尝试写入管道或试图在已关闭写入的套接字上写入
| | +-- ConnectionAbortedError # 连接尝试被对等方中止
| | +-- ConnectionRefusedError # 连接尝试被对等方拒绝
| | +-- ConnectionResetError # 连接由对等方重置
| +-- FileExistsError # 创建已存在的文件或目录
| +-- FileNotFoundError # 请求不存在的文件或目录
| +-- InterruptedError # 系统调用被输入信号中断
| +-- IsADirectoryError # 在目录上请求文件操作(例如 os.remove())
| +-- NotADirectoryError # 在不是目录的事物上请求目录操作(例如 os.listdir())
| +-- PermissionError # 尝试在没有足够访问权限的情况下运行操作
| +-- ProcessLookupError # 给定进程不存在
| +-- TimeoutError # 系统函数在系统级别超时
+-- ReferenceError # weakref.proxy()函数创建的弱引用试图访问已经垃圾回收了的对象
+-- RuntimeError # 在检测到不属于任何其他类别的错误时触发
| +-- NotImplementedError # 在用户定义的基类中,抽象方法要求派生类重写该方法或者正在开发的类指示仍然需要添加实际实现
| +-- RecursionError # 解释器检测到超出最大递归深度
+-- SyntaxError # Python 语法错误
| +-- IndentationError # 缩进错误
| +-- TabError # Tab和空格混用
+-- SystemError # 解释器发现内部错误
+-- TypeError # 操作或函数应用于不适当类型的对象
+-- ValueError # 操作或函数接收到具有正确类型但值不合适的参数
| +-- UnicodeError # 发生与Unicode相关的编码或解码错误
| +-- UnicodeDecodeError # Unicode解码错误
| +-- UnicodeEncodeError # Unicode编码错误
| +-- UnicodeTranslateError # Unicode转码错误
+-- Warning # 警告的基类
+-- DeprecationWarning # 有关已弃用功能的警告的基类
+-- PendingDeprecationWarning # 有关不推荐使用功能的警告的基类
+-- RuntimeWarning # 有关可疑的运行时行为的警告的基类
+-- SyntaxWarning # 关于可疑语法警告的基类
+-- UserWarning # 用户代码生成警告的基类
+-- FutureWarning # 有关已弃用功能的警告的基类
+-- ImportWarning # 关于模块导入时可能出错的警告的基类
+-- UnicodeWarning # 与Unicode相关的警告的基类
+-- BytesWarning # 与bytes和bytearray相关的警告的基类
+-- ResourceWarning # 与资源使用相关的警告的基类。被默认警告过滤器忽略。

详细说明参考:https://docs.python.org/3/library/exceptions.html#base-classes

三、异常捕获

我们不能防止用户输入0, 但在出现异常后我们能捕获并处理异常, 不至于让程序发生终止并退出。 亡羊补牢, 为时未晚。 因此,当发生异常时,我们就需要对异常进行捕获,然后进行相应的处理。python的异常捕获常用try…except…结构,把可能发生错误的语句放在try模块里,用except来处理异常,每一个try,都必须至少对应一个except。此外,与python异常相关的关键字主要有:

关键字 关键字说明
try/except 捕获异常并处理
pass 忽略异常
as 定义异常实例(except MyError as e)
else 如果try中的语句没有引发异常,则执行else中的语句
finally 无论是否出现异常,都执行的代码
raise 抛出/引发异常

异常捕获有很多方式,下面分别进行讨论。

1. try except

异常捕获是通过try-except语句实现的, 基本的try-except语句的语法如下。

1
2
3
4
try:
可能会引发异常的语句
except 异常类型: # 异常类型可省略
处理异常

img

在try代码块中包含在执行过程中可能引发异常的语句, 如果没有发生异常, 则跳到except代码块执行, 这就是异常捕获。try-except语句的执行流程如下。

  • 首先,执行 try 子句(在关键字 try 和关键字 except 之间的语句)。
  • 如果没有异常发生,忽略 except 子句,try 子句执行后结束。
  • 如果在执行 try 子句的过程中发生了异常,那么 try 子句余下的部分将被忽略。如果异常的类型和 except 之后的名称相符,那么对应的 except 子句将被执行。
  • 如果一个异常没有与任何的 except 匹配,那么这个异常将会传递给上层的 try 中。

示例代码

1
2
3
4
5
6
7
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
print(f'{a} / {b} = {res}')
except:
print('除数不能为0, 异常')

如果不指定具体的异常类型,except可以捕获在try中发生的所有异常信息。如果指定异常类型,则except只能捕获对应的异常类型信息。推荐大家在except中指定具体的异常类型。

修改示例代码如下:

1
2
3
4
5
6
7
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
print(f'{a} / {b} = {res}')
except ZeroDivisionError as e:
print(f'除数不能为0, 异常信息为:{e}')

2. 多个except代码块

多条语句可能引发不同的异常,每一种异常采用不同的处理方式。针对这种情况,我们可以在try后跟对个except语句。语法如下:

1
2
3
4
5
6
7
try:
可能会引发异常的语句
except 异常类型1: # 异常类型可省略
处理异常
except 异常类型2: # 可以处理多个异常
处理异常
...

该异常处理语法规则是:

  • 执行try下的语句,如果引发异常,则执行过程会跳到第一个except语句。
  • 如果第一个except中定义的异常与引发的异常匹配,则执行该except中的语句。
  • 如果引发的异常不匹配第一个except,则会搜索第二个except,允许编写的except数量没有限制。
  • 如果所有的except都不匹配,则异常会传递到下一个调用本代码的最高层try代码中。

示例代码:

1
2
3
4
5
6
7
8
9
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
print(f'{a} / {b} = {res}')
except ZeroDivisionError as e:
print(f'除数不能为0, 异常信息为:{e}')
except ValueError as e:
print(f'输入的是无效数字, 异常信息为:{e}')

3. 多重异常捕获

如果多个except代码块的异常处理过程类似,可以合并处理,这就是多重异常捕获。基本语法如下:

1
2
3
4
try:
可能会引发异常的语句
except (异常类型1,异常类型2,...):
处理异常

示例代码:

1
2
3
4
5
6
7
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
print(f'{a} / {b} = {res}')
except (ZeroDivisionError,ValueError) as e:
print(f'异常发生, 异常信息为:{e}')

4. try except语句嵌套

try-except语句还可以嵌套, 语法格式如下:

1
2
3
4
5
6
7
8
try:
可能会引发异常的语句
try:
可能会引发异常的语句
except 异常类型1:
处理异常
except 异常类型2:
处理异常

修改示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
n1 = int(a)
n2 = int(b)
try:
res = a / a
print(f'{a} / {b} = {res}')
except ZeroDivisionError as e:
print(f'除数不能为0, 异常信息为:{e}')
except ValueError as e:
print(f'输入的是无效数字, 异常信息为:{e}')

5. 万能异常

在python的异常中,有一个万能异常:Exception,有时我们不知道具体的错误类型,但又为了防止程序报错而阻塞,可以使用万能异常Exception处理,他可以捕获任意异常,即:

1
2
3
4
5
6
7
8
9
try:
可能会引发异常的语句
except 异常类型1: # 异常类型可省略
处理异常
except 异常类型2: # 可以处理多个异常
处理异常
...
except Exception(万能异常,可匹配所有异常):
处理异常

示例代码如下:

1
2
3
4
5
6
7
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
print(f'{a} / {b} = {res}')
except Exception as e:
print(f'异常发生, 异常信息为:{e}')

四、finally和else

try/except 语句还有一个可选的 else 子句,如果使用这个子句,那么必须放在所有的 except 子句之后。else 子句将在 try 子句没有发生任何异常的时候执行。如果判断完没有某些异常之后还想做其他事,就可以使用下面这样的else语句。

1
2
3
4
5
6
7
8
9
try:
可能会引发异常的语句
except 异常类型1: # 异常类型可省略
处理异常
except 异常类型2: # 可以处理多个异常
处理异常
...
else:
代码 # try语句中没有异常则执行此段代码

img

示例代码:

1
2
3
4
5
6
7
8
9
10
11
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
except ZeroDivisionError as e:
print(f'除数不能为0, 异常信息为:{e}')
except ValueError as e:
print(f'输入的是无效数字, 异常信息为:{e}')
else:
# 没有发生异常执行此段代码
print(f'{a} / {b} = {res}')

有时在try-except语句中会占用一些资源, 例如打开的文件、 网络连接、 打开的数据库及数据结果集等都会占用计算机资源, 需要程序员释放这些资源。 为了确保这些资源能够被释放, 可以使用finally代码块。在try-except语句后面还可以跟一个finally代码块, 语法如下。

1
2
3
4
5
6
7
8
9
10
11
try:
可能会引发异常的语句
except 异常类型1: # 异常类型可省略
处理异常
except 异常类型2: # 可以处理多个异常
处理异常
...
else:
代码块
finally:
代码块

img

无论是try代码块正常结束还是except代码块异常结束,都会执行finally代码块。

1
2
3
4
5
6
7
8
9
10
11
a = input('请输入1个数字:')
b = input('请输入另1个数字:')
try:
res = int(a) / int(b)
print(f'{a} / {b} = {res}')
except ZeroDivisionError as e:
print(f'除数不能为0, 异常信息为:{e}')
except ValueError as e:
print(f'输入的是无效数字, 异常信息为:{e}')
finally:
print('释放资源...')

五、抛出异常

Python 使用 raise 语句抛出一个指定的异常。

raise语法格式如下:

1
raise [Exception [, args [, traceback]]]

img

以下实例如果 x 大于 5 就触发异常:

1
2
3
x = 10
if x > 5:
raise Exception('x 不能大于 5。x 的值为: {}'.format(x))

执行以上代码会触发异常:

1
2
3
4
Traceback (most recent call last):
File "test.py", line 3, in <module>
raise Exception('x 不能大于 5。x 的值为: {}'.format(x))
Exception: x 不能大于 5。x 的值为: 10

raise 唯一的一个参数指定了要被抛出的异常。它必须是一个异常的实例或者是异常的类(也就是 Exception 的子类)。

如果你只想知道这是否抛出了一个异常,并不想去处理它,那么一个简单的 raise 语句就可以再次把它抛出。

1
2
3
4
5
6
7
8
9
10
try:
raise NameError('HiThere')
except NameError:
print('An exception flew by!')
raise

An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, **in** ?
NameError: HiThere

六、自定义异常

有时候,为了提高代码的可重用性,自己编写了一下代码类库,自己编写了一些异常类。实现自定义异常类,需要继承Exception或其子类,之前我们遇到的ZeroDivisionErrorValueError异常类都输Exception的子类。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ShortInputException(Exception):
def __init__(self,length,atleast):
#super().__init__()
self.length = length
self.atleast = atleast

while True:
try:
a = input("输入一个字符串:")
if len(a) < 3:
raise ShortInputException(len(a),3)
except ShortInputException as result:
print("ShortInputException:输入的长度是%d,长度至少是:%d"%(result.length,result.atleast))
break
else:
print("没有异常")
1
2
输入一个字符串:12
ShortInputException:输入的长度是2,长度至少是:3

当创建一个模块有可能抛出多种不同的异常时,一种通常的做法是为这个包建立一个基础异常类,然后基于这个基础类为不同的错误情况创建不同的子类。

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
class Error(Exception):
"""Base class for exceptions in this module."""
pass

class InputError(Error):
"""Exception raised for errors in the input.

Attributes:
expression -- input expression in which the error occurred
message -- explanation of the error
"""

def __init__(self, expression, message):
self.expression = expression
self.message = message

class TransitionError(Error):
"""Raised when an operation attempts a state transition that's not
allowed.

Attributes:
previous -- state at beginning of transition
next -- attempted new state
message -- explanation of why the specific transition is not allowed
"""

def __init__(self, previous, next, message):
self.previous = previous
self.next = next
self.message = message

大多数的异常的名字都以”Error”结尾,就跟标准的异常命名一样。

七、打印异常信息的方式

方式一:直接打印异常

1
2
3
4
5
6
try:
res = 2 / 0
print(res)
except Exception as e:
print('str: ', e) # str: division by zero
print('repr: ', repr(e)) # repr: ZeroDivisionError('division by zero')

打印的异常信息不够详细,对错误追踪没有多大帮助。这时候异常堆栈信息就派上用场了。

方式二:通过logging模块打印异常

1
2
3
4
5
6
7
8
9
10
11
12
try:
res = 2 / 0
print(res)
except Exception as e:
logging.exception(e)
"""
ERROR:root:division by zero
Traceback (most recent call last):
File "D:/python/Python之路/10 异常处理/打印异常信息.py", line 21, in method2
res = 2 / 0
ZeroDivisionError: division by zero
"""

从异常堆栈信息中我们可以不费力气就找出错误代码是哪一行。

方式三:通过tracback模块打印异常

1
2
3
4
5
6
7
8
9
10
11
try:
res = 2 / 0
print(res)
except Exception:
traceback.print_exc()
"""
Traceback (most recent call last):
File "D:/python/Python之路/10 异常处理/打印异常信息.py", line 36, in method3
res = 2 / 0
ZeroDivisionError: division by zero
"""

我们发现系统默认的异常打印方式就是tracback方式。

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

断言可以在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况,例如我们的代码只能在 Linux 系统下运行,可以先判断当前系统是否符合条件。

img

语法格式如下:

1
assert expression

等价于:

1
2
if not expression:
raise AssertionError

assert 后面也可以紧跟参数:

1
assert expression [, arguments]

等价于:

1
2
if not expression:
raise AssertionError(arguments)

以下为 assert 使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> assert True     # 条件为 true 正常执行
>>> assert False # 条件为 false 触发异常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1==1 # 条件为 true 正常执行
>>> assert 1==2 # 条件为 false 触发异常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError

>>> assert 1==2, '1 不等于 2'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: 1 不等于 2
>>>

以下实例判断当前系统是否为 Linux,如果不满足条件则直接触发异常,不必执行接下来的代码:

1
2
3
4
import sys
assert ('linux' in sys.platform), "该代码只能在 Linux 下执行"

# 接下来要执行的代码

八、简单的小结

程序在运行时可能遭遇无法预料的异常状况,可以使用Python的异常机制来处理这些状况。

Python的异常机制主要包括tryexceptelsefinallyraise这五个核心关键字。

try后面的except语句不是必须的,finally语句也不是必须的,但是二者必须要有一个;except语句可以有一个或多个,多个except会按照书写的顺序依次匹配指定的异常,如果异常已经处理就不会再进入后续的except语句;except语句中还可以通过元组同时指定多个异常类型进行捕获;except语句后面如果不指定异常类型,则默认捕获所有异常;捕获异常后可以使用raise要再次抛出,但是不建议捕获并抛出同一个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。

最后强调一点,不要使用异常机制来处理正常业务逻辑或控制程序流程,简单的说就是不要滥用异常机制。

赞赏一下吧~