Python 异常介绍,以及抛出和捕获异常
关注 1260
Python 异常
在 Python 中,所有异常(表示异常的类)都需要继承自BaseException
或Exception
,这包括 Python 的内置异常,以及由开发人员定义的异常。当然,只有少数异常直接继承自类BaseException
,比如,可导致 Python 解释器(可以简单的理解为 Python 的可执行文件或程序)结束的异常SystemExit
,剩余异常均继承自类Exception
或类Exception
的派生类。
异常基类 BaseException 和 Exception 之间的区别
Exception
是BaseException
类的派生类,他表示不是来自于系统的非正常情况,比如,表示除数为0
的异常ZeroDivisionError
。一般情况下,开发人员仅需要捕获从Exception
类派生的各种异常,如果将捕获的范围扩大到BaseException
,那么可能会导致一些意想不到的问题,比如,在try…except
中执行的语句sys.exit()
无法实现退出 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
的异常,用于说明用户昵称不正确的情况。这些自定义的异常应该继承自类Exception
或其派生类,而不是直接继承BaseException
,原因在于直接继承自BaseException
的异常通常用于表示来自于系统的非正常情况。
无论是继承自哪个类,你所定义的异常均拥有一个具有可变参数args
的构造器,这表示可以使用任意的位置参数来创建异常的实例。
如何为自定义异常命名?
在 Python 中,大部分内置异常的名称都以Error
结尾,开发人员自定义的异常应该遵守此约定。
如何在异常的标准回溯中添加注释?
在 3.11 或更高的版本中,异常拥有一个名称为add_note
的方法,可用于为异常的标准回溯添加注释信息,这些信息可作为异常的更进一步说明。
一旦add_note
方法被调用,名称为__notes__
的 Python 列表将被定义在异常中,他包含了所有通过add_note
方法添加的字符串。
类继承
想要了解更多关于 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”出问题了!不被允许的昵称
捕获处理 Python 异常
对于可能引发异常的代码,你可以使用try…except
语句来捕获并处理异常,只需要将这些代码放置在try
语句之后,并使用except
语句来匹配异常即可。
try:
<try-block>
except <exception-1>:
<except-block-1>
…
except <exception-N>:
<except-block-N>
- try-block 部分
try-block
为可能会引发异常的代码(需要使用空白字符进行缩进),当某个语句抛出异常后,try-block
中剩余的语句将不再被执行。- exception 部分
exception
为用于匹配特定异常的表达式,需要给出一个或多个异常类型,当被抛出的异常或其基类属于这些类型时,表示匹配成功。- except-block 部分
except-block
为处理异常的代码(需要使用空白字符进行缩进),当被抛出的异常与某一个except
语句匹配时,except
语句对应的except-block
部分的代码将被执行。
最多只有一个 except 语句能与被抛出异常匹配
如果try…except
语句拥有多个except
语句,那么与if
语句类似,他们会按照先后顺序进行匹配,当某一个except
语句与被抛出的异常匹配时,其余的except
语句将被忽略。
在下面的示例中,由于异常Exception
是ZeroDivisionError
的基类,因此,第一个except
语句与被引发的异常匹配,第二个except
语句将被忽略,即便他与被引发的异常同样匹配。
try:
# 除数为 0 将引发异常 ZeroDivisionError
num = 1 / 0
except Exception:
# Exception 是 ZeroDivisionError 的基类,这里的代码会被执行
print('匹配到了异常 ZeroDivisionError')
except ZeroDivisionError:
print('无人问津!')
匹配到了异常 ZeroDivisionError
与所有 except 语句均不匹配的异常将被重新抛出
如果一个异常被引发,并且与所有的except
语句均不匹配,那么该异常将作为未处理的异常被重新抛出,这可能导致整个程序因此结束。相反的,如果异常与某个except
语句匹配,那么他将被视为已处理,已处理的异常不会被重新抛出。
在下面的示例中,IOError
并非NameError
的基类,这导致异常没有被处理,他会被重新抛出并最终显示了一些错误信息。
try:
# 访问未定义的 undefined,将引发异常 NameError
print(undefined)
except IOError:
# IOError 不是 NameError 的基类,这里的代码不会执行
print('出现了 IO 错误?不可能')
NameError: name 'undefined' is not defined
引用当前被处理的 Python 异常
在except
语句中,使用as
关键字指定一个与被处理异常绑定的标识符(可将其简单的视为 Python 变量),即可通过该标识符在except
语句相关的代码中访问被处理的异常。比如,IOError as err
,(TypeError,AttributeError) as e
。
此外,在 3.11 或更高版本中,通过sys
模块的exception
函数,同样可在except
语句相关的代码中访问当前被处理的异常。
except 语句会删除与异常绑定的标识符
如果你在某个except
语句中使用了as
关键字,那么as
关键字指定的标识符,将在该except
语句的相关代码执行完毕时被删除。这意味着标识符仅在except
语句中保持其正确性,他不应该与同一命名空间的其他标识符重复,以避免一些不必要的错误。
下面的代码定义了变量err
,他同时是except
语句绑定的标识符,因此在except
语句的相关代码执行完毕后,err
将成为未定义的内容,使用print
函数来显示他会导致错误。
一旦脱离了except
语句,sys
模块的函数exception
将返回空值None
,因为此时不存在正被处理的异常。
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
匹配多个或任意类型的 Python 异常
在多数情况下,一个except
语句只处理一种特定类型的异常,但这不排除一个except
语句处理多种特定类型的异常的可能,要完成此目标,你可以在exception
部分使用元组包含多个异常类型,比如,(TypeError,AttributeError)
。
除了匹配多个异常类型,except
语句同样可以匹配任意异常类型,这需要你将exception
部分留空,即不书写任何表达式。
匹配任意异常类型的 except 语句必须是最后一个 except 语句
当一个except
语句匹配任意类型的异常时,该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'>
处理没有 Python 异常被抛出(引发)的情况
被try…except
语句包括的代码,可能会正确执行而不引发任何异常,你可以使用try…except…else
语句来处理这种情况,并将一部分try
和except
关键字之间的代码,转移到else
语句之后,这会使得被转移的代码不再参与异常的捕获,他们仅在没有任何异常被引发时执行。
try:
<try-block>
except <exception-1>:
<except-block-1>
…
except <exception-N>:
<except-block-N>
else:
<else-block>
- else-block 部分
else-block
为没有异常被引发时执行的代码(需要使用空白字符进行缩进)。
使用 else 语句的前提是至少拥有一个 except 语句
如果要使用else
语句来处理没有异常被引发的情况,那么在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
语句的相关代码没有引发任何异常。
函数run
中的print
函数不会被调用,因为try
语句之后的return
语句将被执行。
def run():
try:
# 执行 return 将导致 else 语句被忽略
return
except:
pass
else:
# 这里的语句不会被执行
print('太好了,没有错误')
run()
完成 Python 异常处理的清理工作
在处理异常的过程中,一些代码需要始终被执行,无论是否有异常被抛出,或异常是否被处理。使用finally
语句可以达成上述目标,该语句之后的代码通常与清理工作有关,比如,关闭打开的文件。
try:
<try-block>
…
finally:
<finally-block>
- finally-block 部分
finally-block
为始终被执行的代码(需要使用空白字符进行缩进)。
使用 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)
将引发异常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
抛出(引发)Python 异常
使用 Python 提供的raise
语句,开发人员可以主动抛出(引发)一个异常,其基本的语法形式如下。
raise <exception>
- exception 部分
exception
是被抛出的异常对象,或异常类型。如果仅给出异常类型,那么将根据该类型隐式创建其对应的实例,比如,raise NameError
的效果等同于raise NameError()
。
如何使用 raise 语句抛出(引发)当前被处理的异常?
在except
语句的相关代码中,你可以将raise
语句的exception
部分留空,这会将当前被处理的异常重新抛出。
需要指出的是,以上做法的效果并不等同于调用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 中的跳转语句将使未处理的异常不再重新抛出
如果finally
语句的相关代码中包含了跳转语句,比如break
,continue
或return
,那么这些跳转语句的执行,将导致未被except
处理的异常不再被重新抛出,即便这些异常是通过raise
语句主动抛出的。
3.8 版本之前,在 Python 的finally
语句的相关代码中,不能使用continue
语句。
在下面的代码中,如果函数no_exception
中的return
语句被执行,那么未处理的类型为AttributeError
的异常将不再被重新抛出。
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
未被处理的异常需要在 finally 语句执行完毕后才能重新抛出
当try
,except
或else
语句的相关代码引发新的不能被处理的异常时,这些异常不会被立即抛出,他们需要等待finally
语句的相关代码的执行。
在下面的代码中,类型为ValueError
的异常将被except
语句处理,而except
之后的raise
语句引发了新的异常,由于新的异常无法得到处理,他会在finally
语句的相关代码执行完毕之后,被重新抛出。
try:
# 抛出异常 ValueError
raise ValueError
except ValueError:
# 抛出新的异常 NameError,该异常无法被处理
raise NameError
finally:
print('执行 finally 中的代码后,才能看到错误信息')
执行 finally 中的代码后,才能看到错误信息
…
ValueError
…
NameError
将已处理的 Python 异常指定为原因
如果一个异常已经被某个except
语句处理,而该except
语句的相关代码引发了新的异常,那么新的异常的__context__
变量,即异常的上下文,将指向已被处理的异常,以表示他们之间的关联。
在下面的示例中,except
使用raise
语句引发了新的类型为ValueError
的异常,他的__context__
变量将指向已被except
处理的类型为ZeroDivisionError
的异常。
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'>
如果新的异常是通过raise…from
语句引发,那么你可以为新的异常指定一个表示原因的异常,这通常说明新的异常是由该异常导致的。
raise…from
语句可以在其他位置使用,但如果位于except
语句的相关代码中,那么表示原因的异常一般被指定为已被except
处理的异常。
raise <newexception> from <causeexception>
- newexception 部分
newexception
是被抛出的异常对象,或异常类型。如果仅给出异常类型,那么将根据该类型隐式创建其对应的实例,比如,raise RuntimeError from ValueError()
的效果等同于raise RuntimeError() from ValueError()
。- causeexception 部分
causeexception
是表示原因的异常对象,或异常类型。如果仅给出异常类型,那么将根据该类型隐式创建其对应的实例,比如,raise RuntimeError() from ValueError
的效果等同于raise RuntimeError() from ValueError()
。
回溯将优先展示表示原因的异常的信息
一旦使用了raise…from
语句,被抛出的异常的__cause__
变量将指向表示原因的异常,__suppress_context__
变量将被设置为True
,已明确的指示应在回溯中采用__cause__
而非__context__
,即展示表示原因的异常的信息,而不是表示上下文的异常的信息,除非__cause__
变量为None
并且__suppress_context__
变量为False
。
在下面的示例中,我们使用raise…from
语句将原因指定为类型为ValueError
的异常,而不是except
语句已经处理的异常,这导致回溯中不再展示类型为ZeroDivisionError
的异常的相关信息。
这里需要指出,虽然代码将异常的__suppress_context__
变量设置为False
,但由于其__cause__
不为None
,因此,回溯中不会显示表示上下文的异常的信息。
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
,这将使得被抛出的异常的__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