URLhttps://learnscript.net/zh/python/exceptions/
    复制链接转到说明  示例

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

    我被代码海扁署名-非商业-禁演绎
    阅读 24:03·字数 7217·发布 

    前提

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

    Python 异常

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

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

    ExceptionBaseException类的派生类,他表示不是来自于系统的非正常情况,比如,表示除数为0的异常ZeroDivisionError。一般情况下,开发人员仅需要捕获从Exception类派生的各种异常,如果将捕获的范围扩大到BaseException,那么可能会导致一些意想不到的问题,比如,在try…except中执行的语句sys.exit()无法实现退出 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的异常,用于说明用户昵称不正确的情况。这些自定义的异常应该继承自类Exception或其派生类,而不是直接继承BaseException,原因在于直接继承自BaseException的异常通常用于表示来自于系统的非正常情况。

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

    如何为自定义异常命名?

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

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

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

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

    类继承

    想要了解更多关于 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”出问题了!不被允许的昵称

    捕获处理 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语句将被忽略。

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

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

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

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

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

    try.py
    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,因为此时不存在正被处理的异常。

    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

    匹配多个或任意类型的 Python 异常

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

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

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

    当一个except语句匹配任意类型的异常时,该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'>

    处理没有 Python 异常被抛出(引发)的情况

    try…except语句包括的代码,可能会正确执行而不引发任何异常,你可以使用try…except…else语句来处理这种情况,并将一部分tryexcept关键字之间的代码,转移到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语句。

    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语句的相关代码没有引发任何异常。

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

    else.py
    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 中的相关代码将在其他跳转语句执行之前执行

    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)将引发异常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

    抛出(引发)Python 异常

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

    raise <exception>

    exception 部分

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

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

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

    需要指出的是,以上做法的效果并不等同于调用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 中的跳转语句将使未处理的异常不再重新抛出

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

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

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

    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

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

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

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

    wait.py
    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的异常。

    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'>

    如果新的异常是通过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,因此,回溯中不会显示表示上下文的异常的信息。

    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,这将使得被抛出的异常的__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