Python3基础篇(十)——异常处理

前言:
阅读这篇文章我能学到什么?
  这篇文章将为你介绍Python3中的异常捕获和处理,如果你看过《代码大全2》会明白为程序设计上异常的处理是多么重要的一件事。如果你希望对它有一些基础的了解,那么请读这篇文章。

1 程序异常处理

  程序异常就是程序的运行结果超出了设计者的预料,程序的运行是“非正常”的执行流程。程序的异常处理其实应该分两个阶段,第一个阶段是异常的检测(识别出异常状态,并区分出是何种异常),第二个阶段是针对特定异常情况应该做何种处理(处理可以是忽略、修正、甚至重启)。变成语言支持异常处理已经不是什么“新鲜”的事了,但还是要提一下早期程序处理异常是用 error code 的方式,即函数或代码段返回故障码,通过故障码来区分异常种类和决定如何处理。这种方式已经日渐淘汰,现在很多编程语言已经对异常处理有了较好的支持,形式通常是 try-catch ,在Python3中是 try-except形式。断言是一种常用的异常处理,它一般用于调试阶段(发行版一般将其关闭)。

1.1 assert(断言)

  也其他语言的断言处理一样,当断言的条件为False时,触发异常进行断言处理。断言用于在程序检测到异常时立即终止程序的运行并抛出此时的异常,不必等到后续运行到程序崩溃,抛出一大堆非根源错误信息。代码大全的防御式编程思想是建议在开发调试阶段使问题尽可能“扩大化”,让小问题也无法被忽视(小问题也导致程序运行终止,这样有利于我们写出健康强壮的代码)。
  我们尝试实现一个功能函数,为它加上断言来捕获一些异常。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
def Myabs(Num):                         #求数的绝对值
if Num >= 0:
RetValue = Num
else:
RetValue = -Num

return RetValue

print(Myabs(1)) #整数
print(Myabs(0))
print(Myabs(-1))
print(Myabs(-1.0)) #浮点数

运行结果:

1
2
3
4
1
0
1
1.0

  这个函数用于求一个数的绝对值,看上去似乎没有问题,因为我们进行了一些简单测试后发现结果符合我们的预期。但是如果这个代码交到客户手中将会出现严重的bug,因为你无法现象客户可能给这个函数传递什么奇葩的内容进去。
进行如下测试:

1
2
3
4
5
6
7
8
9
10
11
12
def Myabs(Num):                         #求数的绝对值
if Num >= 0:
RetValue = Num
else:
RetValue = -Num

return RetValue

#预料之外的输入
#print(Myabs("1")) #输入字符串
#print(Myabs(1 + 2j)) #复数
#print(Myabs([1])) #列表

  这几种非法输入全部都将报错,因为这个功能函数异常很可能导致最后整个程序运行异常。我们需要对输入进行一些限制,现在在讲断言,我们就尝试用断言进行这些异常处理(假设我希望非法输入时程序立即停止运行并告诉我发生了什么异常,不要在非法输出的情况下进行后续处理)。
  assert的用法非常简单:
语法结构:

1
assert expression

expression结果为False时触发断言,它将立即终止程序运行并抛出异常信息。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def Myabs(Num):                         #求数的绝对值
#断言处理,只有Num为int或float时才可以往下运行
assert isinstance(Num, int) or isinstance(Num, float)

if Num >= 0:
RetValue = Num
else:
RetValue = -Num

return RetValue

print(Myabs(1)) #整数
print(Myabs(0))
print(Myabs(-1))
print(Myabs(-1.0)) #浮点数
#预料之外的输入
print(Myabs(1 + 2j)) #复数,运行到这里触发断言程序就运行结束了
print(Myabs("1")) #输入字符串
print(Myabs([1])) #列表

运行结果:

1
2
3
4
5
6
7
8
9
10
1
0
1
1.0
Traceback (most recent call last):
File "C:/Users/think/Desktop/Python_Test/Test.py", line 17, in <module>
print(Myabs(1 + 2j)) #复数,运行到这里触发断言程序就运行结束了
File "C:/Users/think/Desktop/Python_Test/Test.py", line 3, in Myabs
assert isinstance(Num, int) or isinstance(Num, float)
AssertionError

  可以看到编译器其实也检测到类型问题抛出错误信息了,这种错误不是语法错误,编译器不能在编译的时候检查出错误,只能在运行的时候。也不能指望测试的时候能100%覆盖到所有输入可能(当然我这里举的例子很简单,稍微有点经验的程序员都能想到非法输入的异常),加入异常就是为了处理发生预料之外的情况。断言在异常时停止程序运行,使得在小的“问题”也无法被忽略,提醒程序设计者“这里”有“超出预料”发生。可以看到异常信息assert isinstance(Num, int) or isinstance(Num, float),提示我们是程序什么地方在抛出这个异常。

1.2 try异常捕获和处理

  Python3提供了较为良好的异常处理机制。下面具体介绍每种语法结构的用法。

1.2.1 try-except结构

语法结构:

1
2
3
4
try:
<CodeBlock>
except:
<CodeBlock>

  try代码块运行时会监视是否有异常事件抛出(可以是Python3自动抛出的异常,也可以由设计者主动抛出异常)。如果检测到异常抛出会执行except后面的代码块,没有异常则忽略这部分代码。
  还需要注意的是当try中运行到抛出异常位置时,将不会继续执行后续的代码。except后可以接具体的异常类型,一个 try-except结构可以有多个except,一个except后可以写多个异常(要以元组形式写出)。当except后不写异常类型时则表示针对所有异常类型。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
def Function(Num1, Num2):
try: #捕捉try代码块的异常
RetValue = Num1 / Num2
print("Here") #上面除式发生异常时不会运行到这里
except: #发生异常后怎么处理,没有指定异常类型则针对所有类异常
print(f"error: {Num1}, {Num2}")
RetValue = Num1

return RetValue

print(Function(1, 2))
print("-------------------------------")
print(Function(1, 0))

运行结果:

1
2
3
4
5
Here
0.5
-------------------------------
error: 1, 0
1

  Num2为0时Python3会自动抛出异常,这个异常被捕获并执行except后(没有指明针对何种异常类型,是针对所有异常类型)的代码快处理异常。
  Num2为0进行除法的异常是Python3自己抛出的,我们尝试主动抛出异常。抛出异常需要使用关键字raise,后面接抛出的异常类型。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def Function(Num1, Num2):
try: #捕捉try代码块的异常
if Num1 > 100 or Num2 > 100:
raise ValueError #对输入值进行检查,主动抛出异常
print("Here") #抛出异常后后面的代码不会继续执行

RetValue = Num1 / Num2
except ZeroDivisionError: #除数为0异常,为python自动抛出
print("ZeroDivisionError")
RetValue = Num1
except ValueError: #值异常,设计者主动抛出
print("ValueError")
RetValue = 0
except: #针对所有类型异常
print("All error")
RetValue = 0

return RetValue

print(Function(1, 0))
print("-------------------------------")
print(Function(1, 101))
print("-------------------------------")
print(Function(1, "2")) #异常输入

运行结果:

1
2
3
4
5
6
7
8
ZeroDivisionError
1
-------------------------------
ValueError
0
-------------------------------
All error
0

  当try中抛出异常时停止try代码块的执行,开始在except语句中查找能匹配抛出异常的分支(就像if-elif语句那样从上往下找到满足条件的分支执行里面的代码块),找到后就执行相应except后面的代码块。没有写明异常类型的except分支是针对所有异常类型。raise后接需要抛出的异常类型,raise后面的代码不会被执行,因为异常已经抛出了。
  给一个except分支指定多个异常类型。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
def Function(Choose):
try:
if Choose == 1:
raise ValueError
elif Choose == 2:
raise ZeroDivisionError
except (ValueError, ZeroDivisionError): #一个分支处理多种异常,元组形式给出异常类型
print("error")

Function(1)
Function(2)

运行结果:

1
2
error
error

  ValueErrorZeroDivisionError都是Python3的内置异常类型,也可以自己定义异常类型。关于异常类型放在这篇文章的后面讲。

1.2.2 try-except-else结构

   try-except 结构基础上可以继续添加else分支,当try中没有抛出异常时将会执行else后的代码块,若有任何异常抛出则不会执行。注意,except分支和else分支并不是一定“互斥”执行的,若try中抛出异常但是except找不到该类异常匹配的分支,则既不会执行except也不会执行else后的代码块。
语法结构:

1
2
3
4
5
6
try:
<CodeBlock>
except Error:
<CodeBlock>
else:
<CodeBlock>

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def Function(Choose):
try:
if Choose == 1:
raise ValueError
elif Choose == 2:
raise ZeroDivisionError
else:
pass #不抛出异常
except ValueError: #一个分支处理多种异常,元组形式给出异常类型
print("error")
else:
print("No error")

#Function(1) #try抛出异常并且except具有该类异常的匹配分支,不会执行else
#Function(2) #try抛出异常并且except没有该类异常的匹配分支,不会执行else
Function(3)

运行结果:

1
No error

  else分支就是无异常时要做什么处理。

1.2.3 try-except-finally结构

  finally分支不论是否有异常抛出都会被执行。 try-except-finally 结构也可以结合上else分支变成 try-except-else-finally 结构。
语法结构:

1
2
3
4
5
6
try:
<CodeBlock>
except Error:
<CodeBlock>
finally:
<CodeBlock>

或者

1
2
3
4
5
6
7
8
try:
<CodeBlock>
except Error:
<CodeBlock>
else:
<CodeBlock>
finally:
<CodeBlock>

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def Function(Choose):
try:
if Choose == 1:
raise ValueError
else:
pass #不抛出异常
except ValueError: #一个分支处理多种异常,元组形式给出异常类型
print("error")
else:
print("No error")
finally:
print("Always Run")

Function(1)
print("-------------------")
Function(2)

运行结果:

1
2
3
4
5
error
Always Run
-------------------
No error
Always Run

  当有异常时如果except分支里具有该类异常的匹配项,则执行该分支,没有则不执行except分支,但是一定会执行finally分支。当没有异常时,出了执行else分支还要执行finally分支。所以含有finally的异常结构可能有两个分支都被执行。

2 异常结构的嵌套

  与 if-elif 结构类似的,异常结构也能进行嵌套。嵌套时外层异常结构可以将内层异常结构看做代码块,当外层try代码块内的内层异常结构,有异常抛出时(指内层处理不了把异常往外层抛出),会被外层的try捕获,然后在except中寻找匹配的分支执行。当内层能处理自己的异常时,异常不被抛到外层,对外层try来说就是没有异常发生。异常嵌套的执行规则和非嵌套结构一样,把内层异常结构看做代码块就可以了。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def Function(Choose):
try:
try:
if Choose == 1:
raise ValueError
elif Choose ==2:
raise ZeroDivisionError #抛出内层不能处理的异常,则后续代码运行将异常抛给外层
except ValueError:
print("Deal ValueError")
print("Here. In try") #内层try有抛出未处理的异常时,会被外层继续捕获到这个异常,内层代码会停止执行
except ZeroDivisionError:
print("Deal ZeroDivisionError")
print("Here. Out try")

Function(1)
print("------------------------")
Function(2) #内层的print函数不会执行

运行结果:

1
2
3
4
5
6
Deal ValueError
Here. In try
Here. Out try
------------------------
Deal ZeroDivisionError
Here. Out try

  在嵌套异常结构类,内层循环不能处理(except没有匹配的分支)的异常会继续抛出到外层,抛出异常后内层循环的代码会停止往后继续执行。总之就是考虑外层时,把内层看作代码块去理解。
  内层也可以主动将异常抛出给外层处理。
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def Function(Choose):
try:
try:
if Choose == 1:
raise ValueError
except ValueError:
print("raise ValueError")
raise #将此异常继续往上层抛出
except: #处理所有内层异常
print("Deal All error")
print("Here. In try") #内层try有抛出未处理的异常时,会被外层继续捕获到这个异常,内层代码会停止执行
except ValueError:
print("Deal ValueError")
print("Here. Out try")

Function(1)

运行结果:

1
2
3
raise ValueError
Deal ValueError
Here. Out try

3 异常类型

  上面涉及到了两种(ValueErrorZeroDivisionError)Python3内置的异常类型,Python3提供了很多内置的异常类型,比如:

异常类型 含义
SyntaxError 语法错误
TypeError 对类型无效的操作
ValueError 传入无效的参数
OverflowError 数值运算超出最大限制
AssertionError 断言语句失败
AttributeError 对象没有这个属性

  等等,太多了不一一列举。这些异常种类是Python3为我们定义好的,设计者可以自己定义属于自己的异常类。定义异常类需要继承Exception异常基类,自定义的异常类可以继续被继承,它们依然是异常类。
打码示例:

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

class MyException(Exception):
def __init__(self, Msg): #重载构造函数
self.Msg = Msg #故障信息

class MyInputException(MyException):
def __init__(self, Msg, File, FunName, Line): # 重载构造函数
super().__init__(f"MyInputException --File: {File}, --FunName: {FunName} --Line: {Line}, --Msg: {Msg}")

class MyOutputException(MyException):
def __init__(self, Msg, File, FunName, Line): # 重载构造函数
super().__init__(f"MyOutputException --File: {File}, --FunName: {FunName} --Line: {Line}, --Msg: {Msg}")

def Function(Choose):
try:
if Choose == 1:
raise MyInputException("Error Test", sys._getframe().f_code.co_filename, #当前文件名
sys._getframe().f_code.co_name, #当前函数名
sys._getframe().f_lineno) #当前行号
elif Choose == 2:
raise MyOutputException("Error Test", sys._getframe().f_code.co_filename,
sys._getframe().f_code.co_name,
sys._getframe().f_lineno)
except MyException as E:
print(E.Msg)

Function(1)
Function(2)

运行结果:

1
2
MyInputException --File: C:/Users/think/Desktop/Python_Test/Test.py, --FunName: Function --Line: 20, --Msg: Error Test
MyOutputException --File: C:/Users/think/Desktop/Python_Test/Test.py, --FunName: Function --Line: 24, --Msg: Error Test

  MyInputExceptionMyOutputException自定义异常类继承于自定义异常类MyException,总之自定义异常类必须直接或间接继承于Exception类,这是Python3内置的异常基类。as关键字给异常类型取了别名,方便通过别名(对象)访问自定义类的成员变量或方法(比如E.Msg)。