Python 异常介绍,以及抛出和捕获 Python 异常

我被代码海扁署名-非商业-禁演绎
阅读 24:39·字数 7398·发布 
Bilibili 空间
关注 960

前提

阅读本节的前提是已经掌握异常的相关概念,你可以查看编程教程异常,异常处理介绍一节来了解他们。

Python 异常

在 Python 中,所有异常(表示异常的类)都需要继承自BaseExceptionException,这包括 Python 的内置异常,以及由开发人员定义的异常。当然,只有少数 Python 异常直接继承自类BaseException,比如,可导致 Python 解释器(可以简单的理解为 Python 的可执行文件或程序)结束的异常SystemExit,剩余 Python 异常均继承自类Exception或类Exception的派生类。

Python 异常基类 BaseException 和 Exception 之间的区别

ExceptionBaseException类的派生类,他表示不是来自于系统的非正常情况,比如,表示除数为0的 Python 异常ZeroDivisionError。一般情况下,开发人员仅需要捕获从Exception类派生的各种 Python 异常,如果将捕获的范围扩大到BaseException,那么可能会导致一些意想不到的问题,比如,在try…except中执行的语句sys.exit()无法实现退出 Python 解释器的效果。

在下面的示例中,由于我们将捕获范围设置为所有 Python 异常,因此sys模块的exit函数所产生的异常SystemExit,并不会导致 Python 解释器的结束,最后的print函数将被执行。

base.py
import sys

try: sys.exit() except BaseException as e: # 异常 SystemExit 将被捕获 print(f'捕获到了异常 {type(e)}')
# 下面的语句会被执行 print('sys.exit() 似乎没有作用哦!')
捕获到了异常 <class 'SystemExit'>
sys.exit() 似乎没有作用哦!

自定义 Python 异常

虽然 Python 已经提供了足够多的内置异常,但开发人员可能依然希望能够自己定义他们,以完成一些特殊的业务逻辑,比如,定义一个名称为NicknameError的异常,用于说明用户昵称不正确的情况。这些自定义的 Python 异常应该继承自类Exception或其派生类,而不是直接继承BaseException,原因在于直接继承自BaseException的异常通常用于表示来自于系统的非正常情况。

无论是继承自哪个类,你所定义的 Python 异常均拥有一个具有可变参数args的构造器,这表示可以使用任意的位置参数来创建 Python 异常的实例。

如何为自定义 Python 异常命名?

在 Python 中,大部分内置异常的名称都以Error结尾,开发人员自定义的 Python 异常应该遵守此约定。

如何在 Python 异常的标准回溯中添加注释?

在 3.11 或更高的版本中,Python 异常拥有一个名称为add_note的方法,可用于为异常的标准回溯添加注释信息,这些信息可作为异常的更进一步说明。

一旦add_note方法被调用,名称为__notes__的 Python 列表将被定义在异常中,他包含了所有通过add_note方法添加的字符串。

类继承

想要了解更多关于 Python 类继承的内容,你可以查看Python 类继承介绍,以及实现 Python 类继承,多重继承,方法重写一节。

下面,我们定义了一个新的异常NicknameError,并将第一和第二个位置参数规定为用户昵称和提示信息,以通过add_note方法生成一个有用的注释。

custom.py
# 定义一个新的异常 NicknameError
class NicknameError(Exception):

def __init__(self, *args): super().__init__(*args) # 将第一个和第二个位置参数作为用户昵称和提示信息 (nickname, msg) = args # 为异常添加注释 self.add_note(f'糟糕,昵称“{nickname}”出问题了!{msg}') print(self.__notes__)
# 抛出异常 NicknameError raise NicknameError('Test', '不被允许的昵称')
['糟糕,昵称“Test”出问题了!不被允许的昵称']
NicknameError: ('Test', '不被允许的昵称')
糟糕,昵称“Test”出问题了!不被允许的昵称

使用 try…except 语句捕获处理 Python 异常

对于可能引发异常的 Python 代码,你可以使用try…except语句来捕获并处理异常,只需要将这些 Python 代码放置在try语句之后,并使用except语句来匹配异常即可。

try:
    <try-block>
except <exception-1>:
    <except-block-1>

except <exception-N>:
    <except-block-N>

try-block 部分

try-block为可能会引发异常的 Python 代码(需要使用空白字符进行缩进),当某个语句抛出异常后,try-block中剩余的语句将不再被执行。

exception 部分

exception为用于匹配特定异常的表达式,需要给出一个或多个 Python 异常类型,当被抛出的异常或其基类属于这些类型时,表示匹配成功。

except-block 部分

except-block为处理异常的代码(需要使用空白字符进行缩进),当被抛出的 Python 异常与某一个except语句匹配时,except语句对应的except-block部分的代码将被执行。

最多只有一个 except 语句能与被抛出 Python 异常匹配

如果try…except语句拥有多个except语句,那么与 Python 的if语句类似,他们会按照先后顺序进行匹配,当某一个except语句与被抛出的 Python 异常匹配时,其余的except语句将被忽略。

在下面的示例中,由于异常ExceptionZeroDivisionError的基类,因此,第一个except语句与被引发的 Python 异常匹配,第二个except语句将被忽略,即便他与被引发的 Python 异常同样匹配。

try.py
try:
	# 除数为 0 将引发异常 ZeroDivisionError
	num = 1 / 0
except Exception:
	# Exception 是 ZeroDivisionError 的基类,这里的代码会被执行
	print('匹配到了异常 ZeroDivisionError')
except ZeroDivisionError:
	print('无人问津!')
匹配到了异常 ZeroDivisionError

与所有 except 语句均不匹配的 Python 异常将被重新抛出

如果一个 Python 异常被引发,并且与所有的except语句均不匹配,那么该异常将作为未处理的 Python 异常被重新抛出,这可能导致整个程序因此结束。相反的,如果异常与某个except语句匹配,那么他将被视为已处理,已处理的 Python 异常不会被重新抛出。

在下面的示例中,IOError并非NameError的基类,这导致异常没有被处理,他会被重新抛出并最终显示了一些错误信息。

try.py
try:
	# 访问未定义的 undefined,将引发异常 NameError
	print(undefined)
except IOError:
	# IOError 不是 NameError 的基类,这里的代码不会执行
	print('出现了 IO 错误?不可能')
NameError: name 'undefined' is not defined

在 except 语句的相关代码中引用当前被处理的 Python 异常

except语句中,使用as关键字指定一个与被处理异常绑定的标识符(可将其简单的视为 Python 变量),即可通过该标识符在except语句相关的代码中访问被处理的 Python 异常。比如,IOError as err(TypeError,AttributeError) as e

此外,在 3.11 或更高版本中,通过sys模块的exception函数,同样可在except语句相关的代码中访问当前被处理的 Python 异常。

except 语句会删除使用 as 关键字与 Python 异常绑定的标识符

如果你在某个except语句中使用了as关键字,那么as关键字指定的标识符,将在该except语句的相关代码执行完毕时被删除。这意味着标识符仅在except语句中保持其正确性,他不应该与同一命名空间的其他标识符重复,以避免一些不必要的错误。

异常

想要了解命名空间这一概念,你可以查看编程教程异常,异常处理介绍一节。

下面的代码定义了变量err,他同时是except语句绑定的标识符,因此在except语句的相关代码执行完毕后,err将成为未定义的内容,使用print函数来显示他会导致错误。

一旦脱离了except语句,sys模块的函数exception将返回空值None,因为此时不存在正被处理的 Python 异常。

as.py
import sys
# 该变量稍后会被删除
err = 'error'

try: # 除数为 0 将引发异常 ZeroDivisionError 1 / 0 except ZeroDivisionError as err: # 通过标识符 err 和 exception 函数获取的异常对象相同 print(sys.exception() == err)
print(sys.exception()) print(err)
True
None

NameError: name 'err' is not defined

使用 except 语句匹配多个或任意类型的 Python 异常

在多数情况下,一个except语句只处理一种特定类型的 Python 异常,但这不排除一个except语句处理多种特定类型的异常的可能,要完成此目标,你可以在exception部分使用元组包含多个异常类型,比如,(TypeError,AttributeError)

除了匹配多个 Python 异常类型,except语句同样可以匹配任意 Python 异常类型,这需要你将exception部分留空,即不书写任何表达式。

匹配任意 Python 异常类型的 except 语句必须是最后一个 except 语句

当一个except语句匹配任意类型的 Python 异常时,该except语句必须是最后一个except语句,原因很简单,如果他位于其他except语句之前,那么一些except语句将失去被执行的可能。也许,Python 可以从底层打乱except语句的执行顺序,但那样的行为似乎并不明智。

下面的第一个except语句匹配异常TypeErrorAttributeError,第二个except语句匹配其他所有异常。

except.py
import random
import sys

try: # 从多个异常中随机选择一个并引发 errs = [TypeError, AttributeError, ZeroDivisionError, IndexError] err = errs[random.randint(0, 3)] raise err() except (TypeError, AttributeError) as e: # 查看异常的具体类型 print(f'异常 {type(e)}') except: # 既不是 TypeError 也不是 AttributeError 的异常 print(f'其他异常 {type(sys.exception())}')
# 输出结果是随机的
其他异常 <class 'IndexError'>

使用 try…except…else 语句处理没有 Python 异常被抛出(引发)的情况

try…except语句包括的 Python 代码,可能会正确执行而不引发任何异常,你可以使用try…except…else语句来处理这种情况,并将一部分tryexcept关键字之间的代码,转移到else语句之后,这会使得被转移的 Python 代码不再参与异常的捕获,他们仅在没有任何 Python 异常被引发时执行。

try:
    <try-block>
except <exception-1>:
    <except-block-1>

except <exception-N>:
    <except-block-N>
else:
    <else-block>

else-block 部分

else-block为没有 Python 异常被引发时执行的代码(需要使用空白字符进行缩进)。

使用 else 语句的前提是至少拥有一个 except 语句

如果要使用else语句来处理没有 Python 异常被引发的情况,那么在try语句之后,需要书写至少一个except语句。

else.py
try:
	import random
	# 有一定的几率引发异常 Exception
	if random.randint(0, 1):
		raise Exception()
except:
	# 至少需要一个 except,才能书写 else
	print('哎呀!一个错误')
else:
	# 仅在没有异常时执行
	print('居然没有错误!')
# 输出结果是随机的
居然没有错误!

执行 try 语句相关代码中的跳转语句将导致 else 语句被忽略

try语句的相关代码中的returncontinuebreak语句被执行时,else语句将被忽略,即便整个try语句的相关代码没有引发任何 Python 异常。

函数run中的print函数不会被调用,因为try语句之后的return语句将被执行。

else.py
def run():
	try:
		# 执行 return 将导致 else 语句被忽略
		return
	except:
		pass
	else:
		# 这里的语句不会被执行
		print('太好了,没有错误')

run()

使用 finally 语句完成 Python 异常处理的清理工作

在处理 Python 异常的过程中,一些代码需要始终被执行,无论是否有 Python 异常被抛出,或 Python 异常是否被处理。使用finally语句可以达成上述目标,该语句之后的代码通常与清理工作有关,比如,关闭打开的文件。

try:
    <try-block>

finally:
    <finally-block>

finally-block 部分

finally-block为始终被执行的 Python 代码(需要使用空白字符进行缩进)。

使用 finally 语句后 except 语句将成为可选的

如果书写了finally语句,那么except语句将不再是必须的,try语句之后可以没有任何except语句。

finally 语句的相关代码将在其他跳转语句执行之前执行

tryexceptelse语句的相关代码中存在某些跳转语句时,比如breakcontinuereturn,与finally语句相关的代码将在这些跳转语句执行之前被执行。

finally.py
def show():
	try:
		# 返回之前会执行 finally 中的代码
		return '一条消息'
	finally:
		# 在真正返回之前,这里的代码将被执行
		print(f'在返回之前执行!')

print(show())
在返回之前执行!
一条消息

finally 语句相关代码中的 return 语句的返回值将取代其他返回值

如果finally语句的相关代码中包含了return语句,那么该return语句所返回的值(包括空值None),将取代tryexceptelse语句相关代码中的返回值。

在下面的代码中,调用div(2, 0)将引发 Python 异常ZeroDivisionError,但语句return 0并不能让函数的返回值成为0,因为finally包含了自己的return语句,div(2, 0)的最终返回值将是2.0

finally.py
def div(a, b):
	try:
		# 如果除数为 0,则引发异常 ZeroDivisionError
		return a / b
	except ZeroDivisionError:
		# 这里的返回值将被忽略
		return 0
	finally:
		# 这里是最终的返回值
		return a / (b + 1)

print(div(2, 0))
2.0

使用 raise 语句抛出(引发)Python 异常

使用 Python 提供的raise语句,开发人员可以主动抛出(引发)一个 Python 异常,其基本的语法形式如下。

raise <exception>

exception 部分

exception是被抛出的 Python 异常对象,或 Python 异常类型。如果仅给出异常类型,那么将根据该类型隐式创建其对应的实例,比如,raise NameError的效果等同于raise NameError()

如何使用 raise 语句抛出(引发)当前被处理的 Python 异常?

except语句的相关代码中,你可以将raise语句的exception部分留空,这会将当前被处理的 Python 异常重新抛出。

需要指出的是,以上做法的效果并不等同于调用exception函数的语句raise sys.exception(),或类似于raise err的语句(假设erras关键字绑定的标识符),他们会展示不同的回溯(Traceback)信息。

在下面的示例中,语句raise NameError等同于语句raise NameError()except之后的语句raise,会将已经捕获到的类型为NameError的异常重新抛出。

raise.py
try:
	# 抛出异常 NameError
	raise NameError
except NameError:
	# 在匹配异常之后,重新将其抛出
	raise
NameError

finally 语句相关代码中的跳转语句将使未处理的 Python 异常不再重新抛出

如果finally语句的相关代码中包含了跳转语句,比如breakcontinuereturn,那么这些跳转语句的执行,将导致未被except处理的 Python 异常不再被重新抛出,即便这些异常是通过raise语句主动抛出的。

3.8 版本之前,在 Python 的finally语句的相关代码中,不能使用continue语句。

在下面的代码中,如果函数no_exception中的return语句被执行,那么未处理的类型为AttributeError的 Python 异常将不再被重新抛出。

no_exception.py
def no_exception(r):
	try:
		# 引发异常 AttributeError
		raise AttributeError()
	finally:
		if r:
			print('调用了 return 语句')
			# 这里的 return 语句将导致未处理的异常不被抛出
			return
		else:
			print('没有调用 return 语句')

# 不会显示错误信息 no_exception(True) # 会显示错误信息 no_exception(False)
调用了 return 语句
没有调用 return 语句

AttributeError

未被处理的 Python 异常需要在 finally 语句的相关代码执行完毕后才能重新抛出

tryexceptelse语句的相关代码引发新的不能被处理的 Python 异常时,这些异常不会被立即抛出,他们需要等待finally语句的相关代码的执行。

在下面的代码中,类型为ValueError的异常将被except语句处理,而except之后的raise语句引发了新的 Python 异常,由于新的异常无法得到处理,他会在finally语句的相关代码执行完毕之后,被重新抛出。

wait.py
try:
	# 抛出异常 ValueError
	raise ValueError
except ValueError:
	# 抛出新的异常 NameError,该异常无法被处理
	raise NameError
finally:
	print('执行 finally 中的代码后,才能看到错误信息')
执行 finally 中的代码后,才能看到错误信息

ValueError

NameError

使用 raise…from 语句将已处理的 Python 异常指定为原因

如果一个 Python 异常已经被某个except语句处理,而该except语句的相关代码引发了新的异常,那么新的 Python 异常的__context__变量,即异常的上下文,将指向已被处理的 Python 异常,以表示他们之间的关联。

在下面的示例中,except使用raise语句引发了新的类型为ValueError的异常,他的__context__变量将指向已被except处理的类型为ZeroDivisionError的 Python 异常。

from.py
try:
	try:
		# 引发异常 ZeroDivisionError
		1 / 0
	except:
		# 处理 ZeroDivisionError 之后,引发新的异常 ValueError
		raise ValueError
except Exception as err:
	print(f'{type(err)} 的 __context__ 的类型为 {type(err.__context__)}')
<class 'ValueError'> 的 __context__ 的类型为 <class 'ZeroDivisionError'>

如果新的 Python 异常是通过raise…from语句引发,那么你可以为新的 Python 异常指定一个表示原因的异常,这通常说明新的 Python 异常是由该异常导致的。

raise…from语句可以在其他位置使用,但如果位于except语句的相关代码中,那么表示原因的异常一般被指定为已被except处理的 Python 异常。

raise <newexception> from <causeexception>

newexception 部分

newexception是被抛出的 Python 异常对象,或 Python 异常类型。如果仅给出异常类型,那么将根据该类型隐式创建其对应的实例,比如,raise RuntimeError from ValueError()的效果等同于raise RuntimeError() from ValueError()

causeexception 部分

causeexception是表示原因的 Python 异常对象,或 Python 异常类型。如果仅给出异常类型,那么将根据该类型隐式创建其对应的实例,比如,raise RuntimeError() from ValueError的效果等同于raise RuntimeError() from ValueError()

回溯将优先展示表示原因的 Python 异常的信息

一旦使用了raise…from语句,被抛出的异常的__cause__变量将指向表示原因的 Python 异常,__suppress_context__变量将被设置为True,已明确的指示应在回溯中采用__cause__而非__context__,即展示表示原因的 Python 异常的信息,而不是表示上下文的 Python 异常的信息,除非__cause__变量为None并且__suppress_context__变量为False

在下面的示例中,我们使用raise…from语句将原因指定为类型为ValueError的异常,而不是except语句已经处理的异常,这导致回溯中不再展示类型为ZeroDivisionError的异常的相关信息。

这里需要指出,虽然代码将异常的__suppress_context__变量设置为False,但由于其__cause__不为None,因此,回溯中不会显示表示上下文的 Python 异常的信息。

from.py
try:
	try:
		# 引发异常 ZeroDivisionError
		1 / 0
	except:
		# 引发异常 RuntimeError,但并未将 ZeroDivisionError 作为原因
		raise RuntimeError from ValueError
except Exception as err:
	print(type(err.__context__))
	print(type(err.__cause__))
	print(err.__suppress_context__)
	err.__suppress_context__ = False
	# 重新抛出异常
	raise
<class 'ZeroDivisionError'>
<class 'ValueError'>
True
ValueError

The above exception was the direct cause of the following exception:

RuntimeError

在 raise…from 语句中将 None 指定为原因

raise…from语句的causeexception部分可以是空值None,这将使得被抛出的 Python 异常的__cause__变量为None__suppress_context__变量为True,回溯中显示的信息被简化。

在下面的示例中,我们使用raise…from语句将原因指定为空值None,回溯中显示了更少的信息。

from_none.py
try:
	try:
		# 引发异常 ZeroDivisionError
		1 / 0
	except:
		# 引发异常 RuntimeError,但将原因设置为 None
		raise RuntimeError from None
except Exception as err:
	print(type(err.__context__))
	print(err.__cause__)
	print(err.__suppress_context__)
	# 重新抛出异常
	raise
<class 'ZeroDivisionError'>
None
True

RuntimeError

内容分类

源码

src/zh/exceptions·codebeatme/python·GitHub