Python 异常介绍,以及抛出和捕获异常
关注 1800
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:
# 在匹配异常之后,重新将其抛出
raiseNameErrorfinally 中的跳转语句将使未处理的异常不再重新抛出
如果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