8. 异常处理

异常处理机制已经成为一门现代编程语言的标配,Python 语言中也提供了成熟的异常处理机制。异常机制使得程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。

在Python 中的异常处理机制主要依赖 tryexceptelsefinallyraise 五个关键字:

  • try 关键字中包含的代码块简称 try 块,该代码块中是可能引发异常的代码
  • except 后对应的是异常类型和对应的代码块,表示的是 except 块针对指定的异常类型的处理代码块
  • else 块的代码一般放置在多个 except 块之后,表示的是在不出现以上各种异常类型时要执行 else 块
  • finally 块的代码一般放置在最后,用于回收在 try 块里打开的物理资源,异常机制会保证 finally 块总被执行
  • raise 块的代码用于抛出一个实际的异常,raise 可以单独作为语句使用,抛出一个具体的异常对象

8.1. 什么是异常

首先我们看一个例子,如下代码所示:

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
c = float(a)/float(b)
print("a / b = ", c)

上述代码实现了一个简单的除法的功能,两个参数 a 和 b 是依靠外部输入的,如果输入 “a = 1, b = 2”,则会得到预期的结果:

请输入:a = 1
请输入:b = 2
a / b =  0.5

但是对于外部的输入,可能会输入任意的值,如“a = aaa, b = bbb”,则会引起 ValueError,因为 float() 函数无法将其输入转换成浮点数,此时我们可以使用 alert 或者 if 作为判断:

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	# a 不是数值
	if not a.isdigit():
		a = input("请重新输入:a = ")
		continue
	# b 不是数值
	elif not b.isdigit():
		b = input("请重新输入:b = ")
		continue
	# 都是数值,则推出判断
	else:
		break

c = float(a)/float(b)
print("a / b = ", c)

通过加入判断是否是数值,解决输入是非法字符的问题。当输入为“a = 1, b = 0”,则会引起 ZeroDivisionError,这是因为除法的分母不能为 0,这时继续增加判断,如下代码所示:

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	# a 不是数值
	if not a.isdigit():
		a = input("请重新输入:a = ")
		continue
	# b 不是数值
	elif not b.isdigit() or float(b)==0:
		b = input("请重新输入:b = ")
		continue
	# 都是数值,则推出判断
	else:
		break

c = float(a)/float(b)
print("a / b = ", c)

对于一个程序设计人员来说,需要尽可能预知所有可能发生的情况,尽可能保证程序在所有糟糕的情形下也都可以运行。但在实际的环境下,情况会变得更加复杂,要使得程序能够稳定的运行,需要一种机制保证对异常的处理,这就需要用到异常处理机制。

8.2. 异常处理机制

Python 的异常处理机制可以让程序具有更好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统会自动生成一个 Error 对象来通知程序,从而实现将业务代码和错误处理代码分离开,提供更好的可读性。

8.2.1. 使用 try … except 捕获异常

在 Python 语言中,提供了 try ... except 的格式用于处理异常,其中,业务代码放在 try 块中,异常处理代码放在 except 块中,Python 的异常处理机制的语法结构如下所示:

try:
	# 业务代码
except (Errorl, Error2, ...) as e:
	# 异常处理代码

如果在执行 try 块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给 Python 解释器,这个过程被称为引发异常。当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为捕获异常。如果 Python 解释器找不到捕获异常的 except 块,则运行时环境终止,Python 解释器也将退出。

Python 的一个 except 块可以捕获多种类型的异常。在使用一个 except 块捕获多种类型的异常时,只要将多个异常类用圆括号 () 括起来,中间用逗号隔开 , 即可。

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	try:
		c = float(a)/float(b)
		print("a / b = ", c)
		break
	except(ValueError, ZeroDivisionError) as e:
		a = input("请重新输入:a = ")
		b = input("请重新输入:b = ")

8.2.2. 访问异常信息

如果程序需要在 except 块中访问异常对象的相关信息,则可通过为异常对象声明变量来实现。当 Python 解释器决定调用某个 except 块来处理该异常对象时,会将异常对象赋值给 except 块后的异常变量,程序即可通过该变量来获得异常对象的相关信息。

所有的异常对象都包含了如下几个常用属性和方法:

  • args:该属性返回异常的错误编号和描述字符串
  • errno:该属性返回异常的错误编号
  • strerror:该属性返回异常的描述宇符串
  • with_traceback():该方法可处理异常的传播轨迹信息

如下代码所示:

def foo():
	try:
		f = open("a.txt")
	except Exception as e:
		# 访问异常的错误编号和详细信息
		print(e.args)
		# 访问异常的错误编号
		print(e.errno)
		# 访问异常的详细信息
		print(e.strerror)
foo()

上述代码调用了 Exception 对象的 args 属性,访问异常的错误编号和详细信息,最终的运行结果如下所示:

(2, 'No such file or directory')
2
No such file or directory

8.2.3. else 块

在 Python 的异常处理流程中还可添加一个 else 块,当 try 块没有出现异常时,程序会执行 else 块。 使用方法如下代码所示:

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	try:
		c = float(a)/float(b)
	except(ValueError, ZeroDivisionError) as e:
		a = input("请重新输入:a = ")
		b = input("请重新输入:b = ")
	else:
		print("输入正常")
		print("a / b = ", c)
		break

上述代码为异常处理流程添加了 else 块,当程序中的 try 块没有出现异常时,程序就会执行 else 块。

8.2.4. 使用 finally 回收资源

如果程序在 try 块里打开了一些物理资源,如数据库连接、网络连接和打开文件等,这些物理资源都必须被显式回收。

Python 的垃圾回收机制不会回收任何物理资源,只能回收堆内存中对象所占用的内存。

那么在哪里回收这些物理资源呢?

  • 如果在 try 块里进行资源回收,如果 try 块的某条语句引发了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。
  • 如果在 except 块里资源回收,因为 except 块完全有可能得不到执行,这将导致不能及时回收这些物理资源。

为了保证一定能回收在 try 块中打开的物理资源,异常处理机制提供了 finally 块。不管 try 块中的代码是否出现异常,也不管哪一个 except 块被执行,甚至在 try 块或 except 块中执行了 return 语句,finally 块总会被执行。Python 完整的异常处理语法结构如下:

try:
	# 业务代码
except Exception1 as e:
	# 异常处理块l
except Exception2 as e:
	# 异常处理块2
else:
	# 正常处理块
finally:
	# 资源回收块

在异常处理语法结构中,需要注意以下几点:

  • try 块是必需的,except 块和 finally 块都是可选的
  • 当存在多个 except 块时,捕获父类异常的 except 块应该位于捕获子类异常的 except 块的后面
  • 若存在 try 块,则必须有 except 块和 finally 块其中的一个,或有两者都有
  • 多个 except 块必须位于 try 块之后,finally 块必须位于所有的 except 块之后

具体的使用方法如下代码所示:

import os

f = None
try:
	f = open("a.txt")
except OSError as e:
	print(e.strerror)
finally:
	# 关闭磁盘文件,因收资源
	if f is not None:
		try:
			# 关闭资源
			f.close()
		except OSError as ioe:
			print(ioe.strerror)
	print("执行 finally 块里的资源回收")

上述代码中,在 finally 块中增加了用于回收打开的物理资源。

8.3. 使用 raise 抛出异常

当程序出现错误时,系统会自动抛出异常。除此之外,Python 也允许通过 raise 语句自行抛出异常。

8.3.1. 抛出异常

需要在程序中自行引发异常,则应使用 raise 语句。raise 语句有如下三种常用的用法:

  • raise:单独一个 raise。该语句抛出当前上下文中捕获的异常(比如在 except 块中),或默认抛出 RuntimeError 异常。
  • raise 异常类:raise 后带一个异常类。该语句抛出指定异常类的默认实例。
  • raise 异常对象:抛出指定的异常对象。

实际上,上述三种用法最终都是要抛出一个异常实例(即使指定的是异常类,实际上也是引发该类的默认实例),raise 语句每次只能引发一个异常实例。如利用 raise 主动抛出异常处理上述的除法问题,代码如下:

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	try:
		if not a.isdigit() or not b.isdigit() or float(b) == 0:
			raise
		c = float(a)/float(b)
		print("a / b = ", c)
		break
	except Exception as e:
		print(e.args)
		a = input("请重新输入:a = ")
		b = input("请重新输入:b = ")

执行上述的代码,当输入一个异常输入时,会报如下的错误:

('No active exception to reraise',)

可见,上述代码抛出了 RuntimeError 异常,程序并跳转到异常对应的 except 块,在上述代码中由 except 块来处理该异常。这就说明,不管是系统自动抛出的异常,还是人为抛出的异常, Python 解释器对异常的处理没有任何差别。

也可以在 raise 后带一个异常类,用于抛出指定异常类的默认实例,如下代码所示:

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	try:
		if not a.isdigit() or not b.isdigit():
			raise ValueError
		elif float(b) == 0:
			raise ZeroDivisionError
		c = float(a)/float(b)
		print("a / b = ", c)
		break
	except Exception as e:
		a = input("请重新输入:a = ")
		b = input("请重新输入:b = ")

8.3.2. 自定义异常类

用户自定义异常都应该继承 Exception 基类Exception 的子类,在自定义异常类时基本不需要书写更多的代码,只要指定自定义异常类的父类即可。自定义异常类的代码如下所示:

class TestException(Exception):
	# exception 逻辑

从新改写上述的代码,有如下的代码:

# 自定义一个异常类
class TestException(Exception):
	def __init__(self):
		print("TestException")

# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")

while True:
	try:
		if not a.isdigit() or not b.isdigit() or float(b) == 0:
			raise TestException
		c = float(a)/float(b)
		print("a / b = ", c)
		break
	except Exception as e:
		a = input("请重新输入:a = ")
		b = input("请重新输入:b = ")

当输入一个异常输入时,会报如下的错误:

请输入:a = 1
请输入:b = 0
TestException

8.4. 本章小结

本章主要介绍了 Python 异常处理机制的相关知识,Python 的异常处理主要依赖 tryexceptelsefinallyraise 五个关键字,本章详细讲解了这五个关键字的用法。本章详细介绍了异常捕获的详细处理方法,以及如何使用 raise 根据业务需求引发自定义异常。

本章需要掌握知识点:

  1. 熟练掌握五个关键字的正确使用
  2. 了解如何自行抛出异常