异常基础
在Python中,异常处理是一种非常重要的程序控制机制,用于处理程序运行时发生的错误。通过异常处理,我们可以控制程序遇到错误时的行为,并保证程序的健壯性和稳定性。异常处理允许程序在遇到错误时优雅地恢复。
- 异常 是程序执行过程中发生的错误事件,它会打断正常的程序流程。
- Python有多种内置异常类型(如
ValueError
,TypeError
,FileNotFoundError
等),也支持自定义异常。
异常捕获
使用try...except...
语句块来捕获并处理异常:
try:
# 尝试执行可能引发异常的代码
result = 10 / 0
except ZeroDivisionError:
# 处理特定类型的异常
print("除数不能为0")
捕获多个异常
一个except
子句可以指定多个要捕获的异常类型:
try:
print(AttributeError.value)
except (ZeroDivisionError, AttributeError) as e:
print("(ZeroDivisionError或AttributeError错误")
print(dir(e))
print(e.args)
print(e.with_traceback)
所有异常捕获
使用一个不指定任何具体错误类型的except:
子句可以捕获所有类型的错误:
try:
print(AttributeError.value)
except Exception as e:
print(dir(e))
print(e.args)
print(e.with_traceback)
多个except块
在Python中,当你使用多个except
块来捕获异常时,捕获顺序非常重要。Python会按照你书写except
块的顺序来尝试匹配异常。一旦某个except
块匹配到了异常,Python就会执行该块内的代码,并且忽略之后的except
块。
try:
print(AttributeError.value)
except (AttributeError) as e:
print("优先捕获 AttributeError 错误")
except Exception as e:
print("捕获所有异常")
注意事项:
- 应该从最具体到最一般地排列 except 块。因为如果先写了一个较一般性质(如Exception)的 except 语句,则后面更具体(如ValueError)的 except 语句永远不会执行到。
- 如果没有任何特定异常需要单独处理,可以只使用一个通用性质(如Exception)来捕获所有可能发生的错误。
- 使用多个 except 块时应确保它们之间没有重复覆盖;即每种类型只能由对应专门处理该类型问题逻辑来管理。
else
和finally
- else: 如果没有任何异常被触发,则执行else子句。
- finally: 不管是否触发了任何异常,都会执行finally子句。通常用于资源清理工作。
try:
name = '正常代码'
except Exception as e:
print("捕获所有异常")
else:
print("代码正常执行,没有任何异常")
finally:
print("比如无论代码是否正常执行,最后都需要关闭资源,避免资源泄露")
异常类
在Python中,所有的异常都是从一个名为BaseException
的基类派生出来的。这个基类允许用户定义自己的异常类型。
BaseException
所有内置异常的最顶层基类。通常不直接使用,而是通过它派生出其他异常。
- SystemExit: 解释器请求退出。
- KeyboardInterrupt: 用户中断执行(通常是输入^C)。
- GeneratorExit: 生成器(generator)发生异常来通知退出。
- Exception: 常规错误的基类,大多数内置非系统退出相关的错误都是从这个类派生出来。
捕获 BaseException
会拦截几乎所有类型的异常,这可能会导致意外地处理了一些你并不打算处理的异常,比如用户按下Ctrl+C导致的 KeyboardInterrupt
。这样做可能会隐藏重要的错误信息,使得调试变得更加困难。
为了避免这个问题,应该只捕获你确实需要处理的异常类型。如果想要捕获所有的标准异常(不包括系统退出和信号处理异常),应该捕获 Exception
类,这是所有标准异常的基类。
try:
print(AttributeError.value)
except BaseException as e:
# idea 会提醒 Too broad exception clause
# 意味着你的 except 块捕获了一个非常广泛的异常类型,这通常不是一个好的做法。
print("所有异常类的基类")
Exception
几乎所有内置非系统退出相关错误都继承自这个类。
-
StopIteration: 迭代器没有更多值。
-
StopAsyncIteration: 必须由异步迭代器对象实现,以在异步迭代结束时引发。
-
ArithmeticError: 各种算术错误:
- OverflowError: 计算超过最大限制。
- ZeroDivisionError: 除数为零。
- FloatingPointError: 浮点计算错误。
-
BufferError: 缓冲区相关操作失败。
-
LookupError: 查询不到数据时引发:
- IndexError: 序列中没有此索引(index)。
- KeyError: 映射中没有这个键(key)。
OSError
操作系统产生的异常(例如文件打开失败等),以前称为IOError和EnvironmentError,现在已经统一归入OSError下,并有很多具体子分类:
- FileNotFoundError: 请求一个不存在的文件或目录时引发此错误。
- PermissionError: 尝试运行操作而没有适当权限时引发此错误等等。
其他
- ValueError:传入无效参数时触发
- TypeError:传入对象类型与要求不符合时触发
- RuntimeError:检测到不属于以上分类但又不能明确归因于其他任何一个具体问题时抛出
- ImportError 和 ModuleNotFoundError:导入模块/对象失败时抛出
- KeyError 和 IndexError :访问字典不存在键值、序列不存在索引位置元素时抛出
自定义异常
通过继承Exception类来创建自己的自定义例外:
class MyCustomError(Exception):
def __init__(self, message):
self.message = message
# 使用自定义例外
try:
raise MyCustomError("Something went wrong!")
except MyCustomError as e:
print(e.message)
抛出异常
在Python中,raise
语句用于抛出一个异常。它允许程序员手动引发异常,或者重新抛出已经捕获的异常。
作用
- 引发新的异常:当程序遇到某些不应该发生的情况时,可以使用
raise
语句抛出一个异常,以便通知调用者存在问题。 - 重新抛出异常:在
except
块中捕获异常后,有时需要重新抛出该异常,以便上层调用者能够知道并处理。raise
语句可以在不改变异常信息的情况下重新抛出当前捕获的异常。
用法
-
不带参数:
raise
这种方式会重新抛出当前
except
块捕获的异常。 -
带异常类:
raise SomeException
这种方式会抛出一个新的
SomeException
类型的异常。 -
带异常实例:
raise SomeException("Error message")
这种方式会抛出一个带有指定错误消息的
SomeException
类型的异常实例。 -
带另一个异常:
raise AnotherException from SomeExceptionInstance
这种方式会抛出一个新的
AnotherException
类型的异常,同时保留SomeExceptionInstance
的堆栈跟踪信息。
注意事项
-
异常类必须继承自
BaseException
:在Python中,所有异常都必须继承自BaseException
或其子类。尝试抛出一个非异常对象或继承自BaseException
的类的实例将会导致TypeError
。 -
异常消息应该是字符串:当提供异常消息时,它应该是一个字符串。如果提供非字符串类型的参数,将会导致
TypeError
。 -
使用
raise
时捕获Exception
:在使用raise
语句时,如果你想捕获所有类型的异常,应该使用Exception
而不是BaseException
。捕获BaseException
可能会意外地捕获系统退出和信号处理异常,这通常不是预期的行为。 -
异常链:在Python 3中,使用
from
关键字可以在抛出新异常的同时保留原始异常的堆栈跟踪信息,这被称为异常链。这有助于调试时追踪异常的来源。 -
避免不必要的异常处理:不应该在正常代码路径中无故抛出异常。异常处理应该用于处理真正的异常情况,而不是控制流。
异常链
在Python中,异常链是指在处理一个异常的过程中,如果发生了另一个异常,那么这两个异常就形成了一条“链”。这种机制允许程序员追踪异常发生的完整路径,特别是在复杂的错误处理或者重新抛出(re-raise)场景中非常有用。
从Python 3开始,在引发新的异常时可以使用from
关键字来链接到原始的(或称为先前的)异常。这样做可以提供更多上下文信息,并帮助理解错误是如何从一处传播到另一处的。
示例
class AnotherException(Exception):
pass
try:
raise Exception('原始错误')
except Exception as origin_error:
try:
# 模拟处理异常的过程中又发生了异常
raise AnotherException('另外的错误')
except AnotherException as new_exc:
raise new_exc from origin_error
在上面这个例子中:
- 首先尝试执行某些代码,并捕获可能产生的
Exception
。 - 在处理该
Exception
时(例如记录日志、清理资源等),又遇到了一个新问题导致抛出了另外一个类型为AnotherException
的错误。 - 这时候我们使用
raise new_exc from origin_error
来将两个不同阶段产生的错误链接起来。
当最终用户看到堆栈跟踪信息时,他们将能够看到完整地包含两个相关联错误信息。Python解释器会显示出所有相关联地错误消息和堆栈跟踪信息。
异常链和__cause__属性
当你使用 raise ... from ...
语法创建了一个新地例外并与之前捕获地例外建立联系后, 新例外对象将具有一个 __cause__
属性指向被链接地旧例外对象。通过检查 __cause__
, 可以获取关于先前错误更多详细信息。
class AnotherException(Exception):
pass
try:
raise Exception('原始错误')
except Exception as origin_error:
try:
# 模拟处理异常的过程中又发生了异常
raise AnotherException('另外的错误') from origin_error
except AnotherException as new_exc:
print(type(new_exc.__cause__))
print(new_exc.__cause__)
raise new_exc
默认情况下隐式链
即使没有显式使用 from
, Python 在默认情况下也会隐式地进行链操作。如果在处理一个例外期间触发了第二个,则第二个(最后抛出)例外知道它是由哪个导致它被触发,并且此关系可以通过检查其 __context__
属性来确定。但要注意,默认情况下只有当没有显式设置 cause 时才会设置 context。
try:
raise Exception('原始错误')
except Exception as origin_error:
try:
# 模拟处理异常的过程中又发生了异常
raise AnotherException('另外的错误')
except AnotherException as new_exc:
print(type(new_exc.__context__))
print(new_exc.__context__)
print(type(new_exc.__cause__))
print(new_exc.__cause__)
raise new_exc