如何抛出和捕获多个 Python 异常?Python 异常组介绍

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

前提

阅读本节的前提是已经掌握 Python 异常,你可以查看Python 异常介绍,以及抛出和捕获 Python 异常一节来了解相关信息。

本节所说的捕获多个 Python 异常是指使用except*语句捕获多个 Python 异常,如果你想使用except语句匹配多个或任意类型的异常,那么可以查看使用 except 语句匹配多个或任意类型的 Python 异常一段。

Python 异常组

有些时候,你可能需要同时抛出多个 Python 异常来说明一种非正常情况。比如,由于没有给出正确的值,因此未能计算出一个有效的文件路径,从而导致了读写错误,我们可能会选择同时抛出异常ValueErrorIOError来说明上述情况(当然,抛出一个异常会更加的简单)。

Python 提供了异常组BaseExceptionGroupExceptionGroup,可以让开发人员一次抛出多个 Python 异常,由于异常组本身也是 Python 异常,因此使用raise语句进行抛出没有任何问题。

Python 异常组 BaseExceptionGroup 和 ExceptionGroup 之间的区别

Python 异常组BaseExceptionGroup直接继承自类BaseException,而ExceptionGroup直接继承自类ExceptionBaseExceptionGroup。异常组BaseExceptionGroup可包含其他任意的 Python 异常,而ExceptionGroup仅支持包含Exception或继承自Exception的异常。

创建 Python 异常组

Python 异常组BaseExceptionGroupExceptionGroup提供了以下构造器用于创建一个异常组的实例。

BaseExceptionGroup(message, exceptions)
ExceptionGroup(message, exceptions)

message 参数

message参数是一个用于说明 Python 异常组的字符串。

exceptions 参数

exceptions参数是一个 Python 序列对象(比如,列表,元组),用于包含需要同时抛出的多个异常。如果exceptions对应的序列对象未包含任何 Python 异常,那么将引发异常ValueError。对于ExceptionGroup,如果exceptions对应的序列对象所包含的异常并非Exception或继承自Exception,那么将引发异常TypeError: Cannot nest … in an ExceptionGroup

下面的代码,将一个BaseExceptionGroup异常组包含在了另一个BaseExceptionGroup异常组中,并将BaseException异常包含在ExceptionGroup异常组中,当然,这导致了最终的错误。

base.py
# 在异常组中包含异常组,并使用异常 BaseException
BaseExceptionGroup(
	'异常组 BaseExceptionGroup',
	[BaseExceptionGroup('包含在异常组中的异常组', [BaseException()])]
)

# 在异常组 ExceptionGroup 中使用异常 BaseException ExceptionGroup( '异常组 ExceptionGroup', # ERROR 不能包含 BaseException [Exception(), BaseException()] )
TypeError: Cannot nest BaseExceptions in an ExceptionGroup

Python 异常组 BaseExceptionGroup 的构造器可能返回 ExceptionGroup 对象

在创建 Python 异常组BaseExceptionGroup的实例时,如果参数exceptions中包含的异常的类型均为Exception或其派生类,那么BaseExceptionGroup的构造器将返回一个ExceptionGroup对象,而非BaseExceptionGroup对象。

因此,无论何时,你都可以使用BaseExceptionGroup的构造器来创建 Python 异常组对象,而不用关注所包含的 Python 异常的类型。

由于异常SystemExit直接继承自BaseException,因此,构造器返回一个BaseExceptionGroup对象。由于异常NameError直接继承自Exception,因此,构造器返回一个ExceptionGroup对象。

base_exception.py
# SystemExit 直接继承自 BaseException
err = BaseExceptionGroup('异常组 BaseExceptionGroup', (SystemExit(),))
print(type(err))

# NameError 直接继承自 Exception err = BaseExceptionGroup('异常组 ExceptionGroup', (NameError(),)) print(type(err))
<class 'BaseExceptionGroup'>
<class 'ExceptionGroup'>

通过 Python 异常组抛出(引发)多个异常

如上所述,Python 异常组BaseExceptionGroupExceptionGroup本身也是 Python 异常,可以通过raise语句来抛出(引发)他们。在这一点上,异常组和普通异常没有区别,只不过异常组包含了一组其他的 Python 异常。

在下面的示例中,我们抛出了一个异常组,他包含了 Python 异常ValueErrorNameError

raise.py
# 抛出包含异常 ValueError 和 NameError 的异常组
raise ExceptionGroup('糟糕,又错了', (ValueError(), NameError()))
ExceptionGroup: 糟糕,又错了 (2 sub-exceptions)

ValueError

NameError

捕获处理 Python 异常组

对于 Python 异常组BaseExceptionGroupExceptionGroup,你可以通过try…except来捕获处理他们(作为一个整体),就像其他普通异常一样,当然,这可能使得使用 Python 异常组变得毫无意义。

catch.py
# 将 Python 异常组作为一个整体进行捕获
try:
	raise ExceptionGroup('毫无意义', (IOError(),))
except BaseExceptionGroup as err:
	print(f'捕获的异常类型为:{type(err)}')
捕获的异常类型为:<class 'ExceptionGroup'>

使用 except* 语句捕获处理 Python 异常组中的一个或多个异常

使用except*语句,可以捕获 Python 异常组中某些的异常,这包括嵌套的异常(异常组中的异常组中的异常),只需给出异常的类型即可。从语法形式上讲,except*语句和except语句是类似的,他接受as关键字,可使用元组指定多个 Python 异常类型,可使用sys模块的exception函数获取捕获到的异常(事实上,except*语句中获取的是异常组),不同的是,except*语句无法通过省略类型来匹配异常组中的任意异常。

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

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

Python 异常组中的异常最多只能与一个 except* 语句成功匹配

当 Python 异常组中的某个异常与某个except*语句匹配成功后,该异常不再参与剩余except*语句的匹配,这表示except*语句将依次执行,以匹配异常组中尚未匹配成功的异常。

与 except* 语句成功匹配的异常将被转换为 Python 异常组

如果 Python 异常组中的某些异常与except*语句匹配,那么这些异常将被转换为一个新的 Python 异常组,以供except*语句对应的代码使用。新异常组的类型可能是BaseExceptionGroupExceptionGroup,这需要根据匹配成功的异常的类型来决定,新异常组的message属性与被抛出异常组的message属性的返回值相同,新异常组的exceptions属性包含了匹配成功的一个或多个 Python 异常。

在下面的示例中,第一个except*语句使用元组匹配到了异常组中的NameErrorValueError,由于都继承自Exception,因此他们会被转换为一个ExceptionGroup对象,第二个except*语句匹配到了异常组中的BaseException,他将被转换为一个BaseExceptionGroup对象。

catch.py
try:
	raise BaseExceptionGroup('捕获', [NameError(), ValueError(), BaseException()])
except* (NameError, ValueError) as err:
	# 转换的异常组的类型为 ExceptionGroup
	print(f'异常类型为:{type(err)}{err.message} {err.exceptions}')
except* BaseException:
	# 转换的异常组的类型为 BaseExceptionGroup
	import sys
	# 通过 exception 函数获取转换的异常组
	err = sys.exception()
	print(f'异常类型为:{type(err)}{err.message} {err.exceptions}')
异常类型为:<class 'ExceptionGroup'>,捕获 (NameError(), ValueError())
异常类型为:<class 'BaseExceptionGroup'>,捕获 (BaseException(),)

except* 语句可以与 Python 异常组中某种类型的所有异常成功匹配

except*语句将匹配 Python 异常组中指定类型(或继承自该类型)的所有异常,除非他们已经与之前其他的except*语句匹配。

下面的第一个except*语句,将与异常组中的两个IOErrorFileNotFoundError匹配,第二个except*语句,将只能与剩下的两个Exception匹配。

catch.py
try:
	raise BaseExceptionGroup('匹配多个异常', [Exception(), Exception(), IOError(), IOError(), FileNotFoundError()])
except* IOError as err:
	# 匹配 IOError,FileNotFoundError
	print(err.exceptions)
except* Exception as err:
	# 只能匹配到 Exception
	print(err.exceptions)
# IOError 是 OSError 的别名
(OSError(), OSError(), FileNotFoundError())
(Exception(), Exception())

Python 异常组中未能成功匹配的异常将被重新抛出

当 Python 异常组中的某些异常,包括嵌套的异常,未能与任何except*语句成功匹配时,这些异常将组合成一个新的异常组被重新抛出,并保持原有的嵌套结构(如果有的话),新异常组的message属性为空字符串,类型(BaseExceptionGroupExceptionGroup)由匹配失败的 Python 异常决定。

下面的示例,异常组中的异常NameErrorOSError不与except*语句匹配,因此他们合并为新的异常组被重新抛出。

catch_raise.py
try:
	# 异常 NameError,OSError 会被重新抛出
	try:
		raise BaseExceptionGroup(
			'重新抛出', (
				NameError(), ValueError(),
				BaseExceptionGroup('嵌套的异常', [OSError(), ValueError()])
			))
	except* ValueError:
		# 两个 ValueError 都会被捕获
		pass
except BaseExceptionGroup as err:
	# 异常组包含了 NameError 和嵌套的 OSError
	print(f'被重新抛出的异常:{type(err)} {err.exceptions}')
被重新抛出的异常:<class 'ExceptionGroup'> (NameError(), ExceptionGroup('嵌套的异常', [OSError()]))

except* 语句不能匹配类型为 BaseExceptionGroup 的异常

except语句不同,except*语句不能匹配类型为BaseExceptionGroup的异常,或继承自BaseExceptionGroup的异常。

在下面的代码中,我们尝试使用except*语句匹配ExceptionGroup,这会导致异常TypeError

catch_group.py
try:
	raise BaseExceptionGroup('异常组', [
		ExceptionGroup('子异常组', [ValueError(), NameError()])
	])
# ERROR 不能匹配 ExceptionGroup
except* ExceptionGroup:
	pass
TypeError: catching ExceptionGroup with except* is not allowed. Use except instead.

except* 语句不能与 except 语句同时使用

在使用try语句之后,不能混合使用except*语句和except语句,否则会引发异常SyntaxError: cannot have both 'except' and 'except*' on the same 'try'

catch_mix.py
# ERROR 无法混合使用 except* 和 except
try:
	raise ExceptionGroup('混合使用', [SystemExit()])
except* SystemExit:
	pass
except:
	pass
SyntaxError: cannot have both 'except' and 'except*' on the same 'try'

无法在 except* 语句的相关代码中使用跳转语句

except*语句的相关代码中,无法使用breakcontinuereturn语句,否则将导致异常SyntaxError: 'break', 'continue' and 'return' cannot appear in an except* block

jump.py
def jump():
	try:
		raise BaseExceptionGroup('跳转', [NameError()])
	except* NameError:
		# ERROR 不能使用跳转语句
		return

jump()
SyntaxError: 'break', 'continue' and 'return' cannot appear in an except* block

使用 except* 语句捕获处理 Python 普通异常

使用except*语句,不仅可以捕获 Python 异常组中的异常,也可以捕获被抛出的普通异常,只不过这些普通异常将被转换为一个异常组,异常组的message属性的值为空字符串。

在下面的代码中,我们引发了一个普通的类型为NameError的异常,该异常将被转换为类型为ExceptionGroup的异常组。

catch_common.py
# 捕获普通的异常
try:
	raise NameError()
except* NameError as err:
	# NameError 被转换为异常组
	print(f'{type(err)} 消息:“{err.message}”,异常:{err.exceptions}')
<class 'ExceptionGroup'> 消息:“”,异常:(NameError(),)

获取 Python 异常组的信息

通过 Python 异常组BaseExceptionGroupExceptionGroup的属性message,可以获取异常组的信息,即构造器的message参数的值。

获取 Python 异常组中的所有异常

Python 异常组BaseExceptionGroupExceptionGroup的属性exceptions是一个 Python 元组,他包含了该异常组中的所有异常。

info.py
try:
	# 由于所包含的异常均继承自 Exception,因此会得到一个 ExceptionGroup 对象
	raise BaseExceptionGroup(
		'这仅仅是一个测试!',
		[NameError(), ValueError()]
	)
except ExceptionGroup as err:
	# 显示异常组的相关内容
	print(f'来自异常组的信息:{err.message}')
	print(f'异常组中的异常:{err.exceptions}')
来自异常组的消息:这仅仅是一个测试!
异常组中的异常:(NameError(), ValueError())

获取 Python 异常组中符合条件的异常

Python 异常组BaseExceptionGroupExceptionGroup对象的subgroup方法,可用于获取异常组中符合指定条件的异常(包括嵌套的异常),并作为一个新的异常组(会保持原有嵌套结构和message属性值)返回,异常组的类型由获取的异常决定,这种效果类似于通过except*语句捕获异常。如果异常组中没有符合条件的异常,那么subgroup方法返回空值None

subgroup方法类似,Python 异常组BaseExceptionGroupExceptionGroup对象的split方法,同样可用于获取异常组中符合指定条件的异常,只不过split方法还会计算异常组中不符合条件的异常,并返回元组(match,rest),其中match等同于subgroup方法的计算结果,rest为包含不符合条件的异常的异常组(会保持原有嵌套结构和message属性值),如果没有不符合条件的异常,那么rest为空值None

baseexceptiongroup|exceptiongroup.subgroup(condition)
baseexceptiongroup|exceptiongroup.split(condition)

condition 参数

condition参数是需要获取的 Python 异常的类型,或包含需要获取的 Python 异常的类型的元组,或用于判断异常是否符合条件的函数或方法(返回True表示符合条件)。与使用except*语句不同,condition参数可以是BaseExceptionGroup的派生类。

在下面的代码中,由于异常组err不包含异常AttributeError,因此err.subgroup(AttributeError)返回了空值Nonesplit方法使用了函数get_error来判断异常是否符合条件。

get.py
err = ExceptionGroup('获取异常', (
	NameError(), ValueError(),
	BaseExceptionGroup('子异常组', [OSError(), NameError()])
))

# 返回 None print(err.subgroup(AttributeError)) # 获取异常组中的 NameError 和 ValueError group = err.subgroup((NameError, ValueError)) print(f'{group.message} {group.exceptions}')
# 判断异常是否符合条件的函数 get_error def get_error(err): return type(err) in (NameError, ValueError)
# 通过函数获取异常组中的 NameError 和 ValueError,以及剩余异常 m, r = err.split(get_error) print(f'{m.message} {m.exceptions} {r.message} {r.exceptions}')
None
获取异常 (NameError(), ValueError(), ExceptionGroup('子异常组', [NameError()]))
获取异常 (NameError(), ValueError(), ExceptionGroup('子异常组', [NameError()])) 获取异常 (ExceptionGroup('子异常组', [OSError()]),)

根据 Python 异常组创建新的异常组

使用 Python 异常组BaseExceptionGroupExceptionGroup对象的derive方法,可依据原有异常组创建新的异常组,新的异常组和原有异常组可以包含不同的异常,而其他内容会被保留,比如,__traceback____cause____context____notes__

derive方法会被subgroupsplit方法使用,以实现其功能。

baseexceptiongroup|exceptiongroup.derive(excs)

excs 参数

excs参数是一个包含了异常的 Python 序列对象,这些异常将被包含在新创建的异常组中。

在下面的示例中,我们通过derive方法创建了新的异常组,并指定ValueError为其包含的异常。

new.py
err = ExceptionGroup('异常组', [NameError()])
new_err = err.derive([ValueError()])

# 显示原有异常组和新异常组 print(f'{err.message} {err.exceptions}') print(f'{new_err.message} {new_err.exceptions}')
异常组 (NameError(),)
异常组 (ValueError(),)

源码

src/zh/exceptions/groups·codebeatme/python·GitHub