Python 异常介绍,以及抛出和捕获 Python 异常
Python 异常
在 Python 中,所有异常(表示异常的类)都需要继承自BaseException
或Exception
,这包括 Python 的内置异常,以及由开发人员定义的异常。当然,只有少数 Python 异常直接继承自类BaseException
,比如,可导致 Python 解释器(可以简单的理解为 Python 的可执行文件或程序)结束的异常SystemExit
,剩余 Python 异常均继承自类Exception
或类Exception
的派生类。
Python 异常基类 BaseException 和 Exception 之间的区别
Exception
是BaseException
类的派生类,他表示不是来自于系统的非正常情况,比如,表示除数为0
的 Python 异常ZeroDivisionError
。一般情况下,开发人员仅需要捕获从Exception
类派生的各种 Python 异常,如果将捕获的范围扩大到BaseException
,那么可能会导致一些意想不到的问题,比如,在try…except
中执行的语句sys.exit()
无法实现退出 Python 解释器的效果。
在下面的示例中,由于我们将捕获范围设置为所有 Python 异常,因此sys
模块的exit
函数所产生的异常SystemExit
,并不会导致 Python 解释器的结束,最后的print
函数将被执行。
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
方法生成一个有用的注释。
# 定义一个新的异常 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
语句将被忽略。
在下面的示例中,由于异常Exception
是ZeroDivisionError
的基类,因此,第一个except
语句与被引发的 Python 异常匹配,第二个except
语句将被忽略,即便他与被引发的 Python 异常同样匹配。
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:
# 访问未定义的 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 异常。
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
语句匹配异常TypeError
或AttributeError
,第二个except
语句匹配其他所有异常。
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
语句来处理这种情况,并将一部分try
和except
关键字之间的代码,转移到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
语句。
try:
import random
# 有一定的几率引发异常 Exception
if random.randint(0, 1):
raise Exception()
except:
# 至少需要一个 except,才能书写 else
print('哎呀!一个错误')
else:
# 仅在没有异常时执行
print('居然没有错误!')
# 输出结果是随机的
居然没有错误!
执行 try 语句相关代码中的跳转语句将导致 else 语句被忽略
当try
语句的相关代码中的return
,continue
或break
语句被执行时,else
语句将被忽略,即便整个try
语句的相关代码没有引发任何 Python 异常。
函数run
中的print
函数不会被调用,因为try
语句之后的return
语句将被执行。
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 语句的相关代码将在其他跳转语句执行之前执行
当try
,except
或else
语句的相关代码中存在某些跳转语句时,比如break
,continue
和return
,与finally
语句相关的代码将在这些跳转语句执行之前被执行。
def show():
try:
# 返回之前会执行 finally 中的代码
return '一条消息'
finally:
# 在真正返回之前,这里的代码将被执行
print(f'在返回之前执行!')
print(show())
在返回之前执行!
一条消息
finally 语句相关代码中的 return 语句的返回值将取代其他返回值
如果finally
语句的相关代码中包含了return
语句,那么该return
语句所返回的值(包括空值None
),将取代try
,except
或else
语句相关代码中的返回值。
在下面的代码中,调用div(2, 0)
将引发 Python 异常ZeroDivisionError
,但语句return 0
并不能让函数的返回值成为0
,因为finally
包含了自己的return
语句,div(2, 0)
的最终返回值将是2.0
。
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
的语句(假设err
为as
关键字绑定的标识符),他们会展示不同的回溯(Traceback)信息。
在下面的示例中,语句raise NameError
等同于语句raise NameError()
,except
之后的语句raise
,会将已经捕获到的类型为NameError
的异常重新抛出。
try:
# 抛出异常 NameError
raise NameError
except NameError:
# 在匹配异常之后,重新将其抛出
raise
NameError
finally 语句相关代码中的跳转语句将使未处理的 Python 异常不再重新抛出
如果finally
语句的相关代码中包含了跳转语句,比如break
,continue
或return
,那么这些跳转语句的执行,将导致未被except
处理的 Python 异常不再被重新抛出,即便这些异常是通过raise
语句主动抛出的。
3.8 版本之前,在 Python 的finally
语句的相关代码中,不能使用continue
语句。
在下面的代码中,如果函数no_exception
中的return
语句被执行,那么未处理的类型为AttributeError
的 Python 异常将不再被重新抛出。
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 语句的相关代码执行完毕后才能重新抛出
当try
,except
或else
语句的相关代码引发新的不能被处理的 Python 异常时,这些异常不会被立即抛出,他们需要等待finally
语句的相关代码的执行。
在下面的代码中,类型为ValueError
的异常将被except
语句处理,而except
之后的raise
语句引发了新的 Python 异常,由于新的异常无法得到处理,他会在finally
语句的相关代码执行完毕之后,被重新抛出。
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 异常。
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 异常的信息。
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
,回溯中显示了更少的信息。
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