- Python 3.11.5 中文文档、cpython源代码
- 错误和异常、内置异常
异常是指在程序执行过程中发生的不寻常或错误的事件。当程序在运行中发生异常时,Python会生成一个异常对象,其中包含了关于异常的信息,比如异常类型、发生异常的代码位置等。
异常堆栈跟踪是一份详细的报告,它记录了异常是如何传播的,从异常发生的地方开始,一直到最初的异常引发点。这份报告以栈的形式呈现,因此被称为“堆栈跟踪”。下面是一个常见的除0错误示例:
result = 10 / 0 # 这会引发一个除零异常
result
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[16], line 1
----> 1 result = 10 / 0 # 这会引发一个除零异常
2 result
ZeroDivisionError: division by zero
在这个示例中,会发生以下操作:
解析器(Python解释器)会尝试解析您的代码以执行它,但在遇到除0错误时,解析器会停止执行并生成一个错误消息。
这个错误消息是一个典型的Python异常堆栈跟踪:
ZeroDivisionError
: 这是异常的类型,表示发生了一个除零错误。Traceback (most recent call last)
: 这一行告诉我们以下内容是异常堆栈跟踪的开始。Cell In[16], line 1
: 这一行显示了发生异常的代码位置。具体来说,在 “Cell In[16]” 中的第1行发生了异常。result = 10 / 0
: 这是尝试执行的代码,它试图将10除以0,但由于除零错误,引发了异常。result
: 这是在异常发生后的下一行的代码,但由于异常在前一行引发,所以这一行实际上不会执行。总结起来,这个错误消息告诉我们在尝试将10除以0时发生了除零错误(ZeroDivisionError)。异常堆栈跟踪还显示了异常发生的确切代码位置,这对于调试问题非常有帮助。
下面是一个稍微复杂一点的堆栈跟踪示例:
Traceback (most recent call last):
File "main.py", line 10, in <module>
division_result = divide(10, 0)
File "main.py", line 6, in divide
result = numerator / denominator
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "main.py", line 12, in <module>
log_exception()
File "main.py", line 8, in log_exception
raise Exception("An error occurred while logging the exception.")
Exception: An error occurred while logging the exception.
在这个示例中,发生了两个异常,并且堆栈跟踪显示了异常传播的路径:
最初的异常发生在 “main.py” 文件的第10行,其中我们尝试调用 divide(10, 0)
函数,但由于除以零,引发了 ZeroDivisionError
异常。这是堆栈跟踪的起始点。
堆栈跟踪接着显示了异常处理程序的执行情况。在 log_exception()
函数中,我们尝试记录异常,但由于代码中引发了另一个异常,所以出现了第二个异常。第二个异常是一个通用的 Exception
,并且在堆栈跟踪中显示了详细信息。
从上述示例可以看出,异常堆栈跟踪有以下几个重要作用:
定位异常引发点: 它告诉我们异常是在哪个位置引发的。这对于找到问题的根本原因至关重要。
追踪异常传播路径: 堆栈跟踪显示了异常是如何传播的,从引发异常的地方到最终导致程序中止的地方,这有助于我们理解异常如何影响程序的执行流程。
有时候堆栈跟踪可能会非常复杂,特别是在大型项目中,但基本的原则是相同的:从底部向上查看,以了解异常的源头和传播路径。
为了避免异常导致程序终止,我们引入了异常处理操作,它可以:
在Python等编程语言中,异常处理通常通过以下关键字和结构来实现:
try
:try
语句通常包含可能引发异常的代码,当尝试执行时,若无异常,则跳过 except 子句,执行后续语句
except
:捕获异常,并定义相应的异常处理逻辑,而不会导致程序的终止。
在try
代码块中发生异常时,except中的语句会被执行,即判断异常类型是否与 except 关键字后指定的异常相匹配:
else
:处理没有异常的情况,所以else 子句必须放在所有 except 子句 之后。若try
块中的代码成功执行,且没有异常被引发,那么else
中的代码块会被执行。
finally
:无论是否发生异常都会执行 finally
语句块,即使在try
或except
块中有return
语句也不例外。finally
语句通常用于确保资源的正确清理,例如关闭文件或释放网络连接。
通过合理使用这些结构,程序员可以在异常发生时采取适当的措施,以确保程序的稳定性和可维护性,编写出更健壮的应用程序。
下面是一个简单的文件操作异常处理示例,我们尝试打开文件,并在except
块中自定义了相应的异常处理逻辑:
try:
# 将文件操作代码包装在try语句中,以捕获可能发生的异常
with open("file.txt", "r") as file:
content = file.read()
except FileNotFoundError:
# 处理文件不存在的情况
print("文件不存在。")
except PermissionError:
# 处理权限问题
print("没有权限执行此操作。")
except Exception as e:
# 处理其他异常
print("发生错误:", str(e))
else:
# 如果没有异常发生,执行这里的代码
print("文件操作成功。")
finally:
# 无论是否发生异常,都会执行这里的代码
print("文件操作完成。")
在except
块中,你也可以根据需要捕获特定类型的异常。这使您可以更精确地处理不同类型的问题。例如,如果只关心文件不存在的异常,可以捕获FileNotFoundError
。
try:
with open("file.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("文件不存在。")
except
子句 可以用带圆括号的元组来指定多个异常,例如::
except (RuntimeError, TypeError, NameError):
pass
finally
是try
语句的可选子句,用于定义在所有情况下都必须要执行的清理操作(例如释放文件等外部资源),且不论 try
语句是否触发异常,都会执行 finally
子句。以下内容介绍了几种比较复杂的触发异常情景:
如果 finally 子句中包含 break、continue 或 return 等语句,异常将不会被重新引发。
def example():
try:
x = 1 / 0 # 会引发异常
except ZeroDivisionError:
print("Caught an exception")
finally:
print("Finally block executed")
return 42 # 返回值来自 finally 子句,不是来自 try 子句
result = example()
print("Result:", result)
Caught an exception
Finally block executed
Result: 42
如果执行 try 语句时遇到 break,、continue 或 return 语句,则 finally 子句在执行 break、continue 或 return 语句之前执行。
def example():
try:
print("In try block")
return 10
finally:
print("In finally block")
result = example()
print("Result:", result)
In try block
In finally block
Result: 10
如果 finally 子句中包含 return 语句,则返回值来自 finally 子句的某个 return 语句的返回值,而不是来自 try 子句的 return 语句的返回值。
def example():
try:
return 10
finally:
return 20 # 返回值来自 finally 子句,不是来自 try 子句
result = example()
print("Result:", result)
Result: 20
下面再举一个复杂的例子说明:
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("division by zero!")
else:
print("result is", result)
finally:
print("executing finally clause")
divide(2, 1)
result is 2.0
executing finally clause
divide(2, 0)
division by zero!
executing finally clause
divide("2", "1")
executing finally clause
Traceback (most recent call last):
File "" , line 1, in <module>
File "" , line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'
在Python中,异常是通过异常类的实例来表示的。异常类是一种用于组织和表示不同类型异常情况的方式。每个异常类都代表一种特定类型的错误或异常情况,例如ValueError
表示数值错误。当程序中发生异常情况时,Python会创建一个与该异常情况相关的异常类的实例,并将其引发(raise)。
Python为常见的错误情况定义了许多内置异常,以便在程序执行过程中处理错误和异常情况,这些类都继承自基类BaseException
。整个异常类的层级结构如下:
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
...
...
下面对部分异常类进行解释:
主要异常类 | 描述 |
---|---|
BaseException |
所有内置异常类的基类,通常不直接使用,而是派生其他异常类 |
GeneratorExit |
用于当生成器或协程在执行中遇到该异常时,自动关闭。通常用于清理生成器或协程中的资源 |
KeyboardInterrupt |
当用户在命令行中按下Ctrl+C时,会引发此异常,中断正在执行的程序。 |
SystemExit |
当调用sys.exit() 函数时引发的异常,用于退出Python解释器。 |
Exception |
所有内置的非系统退出类的基类,所有用户自定义异常也应当派生自此类 |
常见异常类 | 描述 |
---|---|
SyntaxError |
语法错误 |
IndentationError |
缩进错误 |
ImportError |
导入模块不存在 |
NameError |
名称错误,当尝试访问未定义的变量或函数时引发。 |
AttributeError |
属性错误,当尝试访问对象没有的属性或方法时引发。 |
TypeError |
类型错误,当操作不兼容的数据类型时引发。 |
KeyError |
使用字典中不存在的键时引发。 |
ValueError |
当函数收到的参数类型正确但值不合法时引发。 |
ZeroDivisionError |
零除错误,当试图除以零时引发。 |
FileNotFoundError |
文件未找到错误 |
PermissionError |
文件权限不足错误 |
IndexError |
索引错误,当尝试访问不存在的列表元素时引发。 |
MemoryError |
内存耗尽异常。通常在处理大型数据集时发生。 |
类有继承这一特性,所以若发生的异常与 except 子句中的类是同一个类或是它的基类时,则该类与该异常相兼容,反之则不成立。简单来说,就是子类兼容父类的异常,父类不兼容子类的异常。下面举例说明:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
上面的代码将依次打印 B, C, D。而如果将except子句顺序改为:
try:
raise cls()
except B:
print("B")
except C:
print("C")
except D:
print("D")
则会打印出B,B,B。这是因为C和D都是B的子类,能捕获B类的错误,肯定也会捕获C类和D类,所以三次遍历都会被except B捕获。
内置异常类可以被子类化以定义新的异常,但是在创建自定义异常类时,推荐从Exception
类或其子类来继承,而不是直接继承自BaseException
类。这是因为Exception
是所有非致命异常的基类,而BaseException
类和它的其它子类表示的都是非常严重的异常,通常会导致程序终止,也就不需要进行处理。
Exception
类具有一个构造函数 __init__(self, *args)
,该构造函数允许传递一个或多个参数(通常是异常消息)。你可以在派生的自定义异常类中扩展此构造函数以添加额外的参数,如错误代码、时间戳等。
异常类命名一般都以 “Error” 结尾,与标准异常的命名保持一致。异常类应当保持简单,只提供一些属性,允许相应的异常处理程序提取有关错误的信息。许多标准模块都定义了自己的异常,以报告他们定义的函数中可能出现的错误。
import datetime
# 自定义一个简单的异常类,继承自 Exception
class CustomError(Exception):
# message用于提供异常的描述性消息,error_code用于指定自定义错误代码。
def __init__(self, message, error_code):
# super调用父类 Exception 的构造函数init,并传递 message 参数,以初始化异常的消息。
super().__init__(message)
self.error_code = error_code
# 获取当前时间戳,并将其赋值给异常对象的 timestamp 属性
self.timestamp = datetime.datetime.now()
# 将异常对象的 file_name 属性初始化为 None,目前还没有指定文件名。
self.file_name = None
try:
# 使用 raise 语句抛出了一个 CustomError 异常的实例。
# 异常的消息是 "This is a custom exception",错误代码是 1001。
raise CustomError("This is a custom exception", 1001)
except CustomError as ce:
# 打印了异常的消息、错误代码和时间戳
print(f"Custom error occurred: {ce}, Error code: {ce.error_code}, Timestamp: {ce.timestamp}")
# 自定义FileNotFoundError类,用于捕获异常时获取文件名信息
class FileNotFoundError(Exception):
def __init__(self, file_name):
super().__init__(f"File not found: {file_name}")
file_name = "example.txt"
try:
# 尝试打开文件
with open(file_name, "r") as file:
content = file.read()
except FileNotFoundError as fnfe:
print(fnfe)
class CustomError(Exception):
def __init__(self, message, error_code):
super().__init__(message)
self.error_code = error_code
def log_error(self):
# 将异常信息记录到日志文件
# 打开'error.log' 日志文件,模式为 'a'(追加)
with open('error.log', 'a') as log_file:
log_file.write(f"Error Code: {self.error_code}, Message: {str(self)}\n")
def notify_admin(self):
# 发送电子邮件或其他通知给管理员
pass
try:
raise CustomError("This is a custom exception", 1001)
except CustomError as ce:
ce.log_error()
ce.notify_admin()
print(f"Custom error occurred: {ce}, Error code: {ce.error_code}")
除了使用try...except
语句,你也可以使用 raise
语句来手动引发指定的异常,其唯一的参数就是要触发的异常,但必须是异常实例或异常类。
引发异常实例:您可以使用 raise
语句引发已经创建的某个异常类的实例,这样做的目的是在特定的代码位置引发异常,并且可以提供关于异常的详细信息。
# 创建一个自定义异常类
class CustomError(Exception):
def __init__(self, message):
super().__init__(message)
try:
# 创建异常实例并引发
error_instance = CustomError("This is a custom exception")
raise error_instance
except CustomError as ce:
print(f"Caught custom exception: {ce}")
引发异常类:当您想引发某种标准异常时,但不需要提供额外的异常信息时,可以简单地引发异常类。
try:
# 引发内置的 ValueError 异常类
raise ValueError("This is a ValueError")
except ValueError as ve:
print(f"Caught ValueError: {ve}")
某些时候,我们想要在捕获异常后,继续将同一异常传播到更高级别的异常处理程序,或者让其他部分的代码处理它。这时,我们可以使用 raise 语句来重新引发已捕获的异常:
try:
result = 10 / 0 # 这会引发一个除零异常
except ZeroDivisionError:
print("Divided by zero")
# 当异常在 except 块中被捕获后,可以使用 raise 语句重新引发相同的异常,以便允许其他代码或更高级别的异常处理程序来处理异常。
raise
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[14], line 2
1 try:
----> 2 result = 10 / 0 # 这会引发一个除零异常
3 except ZeroDivisionError:
4 print("Divided by zero")
ZeroDivisionError: division by zero
raise
和except
都用于处理异常,但它们的角色和使用场景是不同的:
except
:用于捕获和处理在 try
块中引发的异常。这是一种被动的操作,它用于响应异常的发生。你可以在except
语句中定义异常处理逻辑,例如指定不同的 except
子句处理不同类型的异常。如果不处理异常,程序将终止并显示异常信息。
raise
:用于在代码中明确指出异常情况的发生,这是一种主动的操作,你可以在任何地方使用它来引发异常。raise
通常用于异常的起始点,它告诉程序出现了异常情况,然后程序的控制权会被传递给异常处理程序(except
子句)。
下面是一个具体的示例。假设一个函数,接受一个数字作为参数,它在参数为负数时应该引发一个自定义异常。这里就是 raise
的使用场景,它用于主动引发异常:
def divide_positive_numbers(a, b):
if a < 0 or b < 0:
raise ValueError("Both numbers should be positive")
return a / b
然后,在调用这个函数时,可以使用 try
和 except
来捕获和处理这个异常:
try:
result = divide_positive_numbers(-5, 2)
except ValueError as e:
print("An error occurred:", e)
在这个示例中,raise
用于在函数内部检测到不符合要求的输入时引发异常,而 except
用于捕获和处理这个异常。这就是 raise
和 except
在异常处理中的不同用途和角色
异常链是指在 Python 中,一个异常引发另一个异常的情况,从而形成一个异常的嵌套链条。这个链条可以用来跟踪异常的源头,以便更好地理解程序中发生的错误,特别是当异常发生在复杂的程序堆栈中时。通过正确使用 raise
语句的 from
子句,可以更好地组织和管理异常链。下面是常见的异常链操作:
使用 from
子句触发异常链: 你可以在 raise
语句中使用 from
子句,将一个异常视为另一个异常的原因。这样,新引发的异常将成为前一个异常的 “直接原因”。
# 示例1:异常的直接后果和异常链
try:
# 引发一个异常
raise ValueError("This is the first exception")
except ValueError as ve:
try:
# 引发另一个异常,将前一个异常作为直接的原因
raise TypeError("This is the second exception") from ve
except TypeError as te:
print("Caught TypeError:", te)
print("Direct cause:", te.__cause__)
在这个示例中,我们首先引发一个 ValueError
异常,然后在 except
块中引发一个 TypeError
异常,并将前一个异常 ve
作为直接的原因。这样,我们就可以在 TypeError
异常中通过 te.__cause__
访问到直接的原因,即 ValueError
异常。
使用 from None
禁用自动异常链:如果在 raise
语句的 from
子句中使用 None
,则会禁止Python自动创建异常链,以减少异常的复杂性。此时,异常会被视为相互独立的。
try:
# 引发一个异常,并禁用自动异常链
raise ValueError("This is an exception") from None
except ValueError as ve:
print("Caught ValueError:", ve)
print("Direct cause:", ve.__cause__)
在这个示例中,异常链被禁用,ve.__cause__
的值为 None
,不再与其他异常相关联。
在某些情况下,可能需要报告多个已经发生的异常,而不仅仅只是报告第一个异常。例如在并发框架等情境中,多个任务并行执行时,可能会发生多个错误。
为了处理这种情况,Python 提供了一种 ExceptionGroup
,它允许将多个异常实例打包成一个列表,从而一起引发,这使得多个异常可以同时被捕获和处理。
def f():
# 创建了一个包含两个异常实例的列表,这两个异常实例都包含了错误消息
excs = [OSError('error 1'), SystemError('error 2')]
raise ExceptionGroup('there were problems', excs)
f()
---------------------------------------------------------------------------
ExceptionGroup Traceback (most recent call last)
Cell In[17], line 5
2 excs = [OSError('error 1'), SystemError('error 2')]
3 raise ExceptionGroup('there were problems', excs)
----> 5 f()
Cell In[17], line 3, in f()
1 def f():
2 excs = [OSError('error 1'), SystemError('error 2')]
----> 3 raise ExceptionGroup('there were problems', excs)
ExceptionGroup: there were problems (2 sub-exceptions)
上述代码使用 raise ExceptionGroup
引发了一个异常,该异常包含一个错误消息 ‘there were problems’ 和前面创建的异常实例列表 excs。这样就创建了一个异常链,其中 ExceptionGroup 是顶级异常,而 OSError 和 SystemError 是其下的子异常。
使用以下代码,可以打印具体的异常信息:
def f():
excs = [OSError('error 1'), SystemError('error 2')]
raise ExceptionGroup('there were problems', excs)
try:
f()
except Exception as e:
print(f'caught {type(e).__name__}: {e}') # 打印异常信息
# 如果异常是 ExceptionGroup,打印其中包含的子异常信息
if isinstance(e, ExceptionGroup):
for i, exc in enumerate(e.exceptions, 1):
print(f'Exception {i}: {type(exc).__name__}: {exc}')
caught ExceptionGroup: there were problems (2 sub-exceptions)
Exception 1: OSError: error 1
Exception 2: SystemError: error 2
对于一个嵌套的异常组,可以使用except*
子句从组中提取了某种类型的异常,而让所有其他的异常传播到其他子句,并最终被重新引发。另外,嵌套在一个异常组中的异常必须是实例,而不是类型。这是因为在实践中,这些异常通常是那些已经被程序提出并捕获的异常。此部分详细内容,请参考《错误和异常》。
在异常被捕获后,有时需要向异常对象添加额外的信息,以便更好地描述错误和提供上下文信息。Python 的异常对象具有 add_note(note)
方法,允许您将字符串注释添加到异常对象的注释列表中。当异常被引发后,异常对象中的注释会按照它们被添加的顺序显示在标准异常回溯信息中,例如:
try:
raise TypeError('bad type')
except Exception as e:
e.add_note('Add some information')
e.add_note('Add some more information')
raise
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[21], line 2
1 try:
----> 2 raise TypeError('bad type')
3 except Exception as e:
4 e.add_note('Add some information')
TypeError: bad type
Add some information
Add some more information
你也可以在自定义异常中,添加额外的异常注释:
class CustomError(Exception):
def __init__(self, message):
super().__init__(message)
self.notes = [] # 初始化注释列表
def add_note(self, note):
self.notes.append(note) # 添加注释到异常对象的注释列表中
def divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError as zd_err:
# 创建自定义异常并添加注释
custom_err = CustomError("Division error")
custom_err.add_note("Attempted to divide by zero")
raise custom_err
try:
result = divide(10, 0)
except CustomError as ce:
print("Caught CustomError:", ce)
print("Notes:")
for note in ce.notes:
print("-", note)
Caught CustomError: Division error
Notes:
- Attempted to divide by zero
在这个示例中,divide
函数尝试执行除法操作,但如果除法操作失败(除以零),它会引发一个自定义异常 CustomError
。在这个自定义异常中,我们使用 add_note
方法添加了两个注释,分别描述了错误的性质和错误的上下文。最后,在异常处理代码中,我们遍历异常对象的注释列表,以查看所有添加的注释。
在编写Python代码时,异常处理是一个关键的方面,它有助于增加代码的稳定性和可靠性,能够处理各种不可预测的情况,增加代码的健壮性。以下是一些建议:
仅捕获您知道如何处理的异常,以便精确处理问题
在异常处理中,应该只捕获那些您知道如何处理的异常。不要仅仅因为可能发生异常就捕获所有异常。这可能会导致隐藏真正的问题,使调试变得更加困难。只捕获你能够处理的异常,对于其他异常,应该让它们引发并停止程序的执行。
引发具有明确描述的自定义异常
当密需要在代码中指示特定的错误条件时,应该引发自定义异常。这些异常应该具有清晰的描述,以便其他开发人员能够理解问题的本质。例如:
if condition:
raise CustomException("This is a custom exception message.")
不要捕获 Exception
的通用异常
避免捕获通用的 Exception
异常,因为这样会捕获所有异常,包括系统退出等。这可能导致程序的行为不可预测。应该捕获具体的异常类型,例如 ValueError
、FileNotFoundError
等。
记录异常信息
当捕获异常时,应该记录异常信息,以便后续调试。可以使用标准库中的 logging
模块来记录异常信息,以便更容易诊断问题。例如:
import logging
# 配置日志记录,将日志保存到名为myapp.log的文件中,只记录ERROR级别及以上的日志,并使用特定的格式
logging.basicConfig(filename='myapp.log', level=logging.ERROR, format='%(asctime)s [%(levelname)s] %(message)s')
try:
result = 10 / 0 # 这会引发一个除零异常
except ZeroDivisionError as e: # 捕获ZeroDivisionError异常,并将其赋给变量e
logging.error("An error occurred: %s", e) # 使用logging.error()记录错误级别的日志,包括异常信息
上述代码中,format 参数用于定义日志消息的格式,它是一个包含占位符的字符串,这些占位符将在实际记录日志消息时被替换为相应的值。常用参数包括:
占位符 | 含义 |
---|---|
%s |
用于插入字符串。 |
%d |
用于插入整数。 |
%f |
用于插入浮点数。 |
%r |
用于插入表示对象的字符串,通常是 repr(object) 的结果。 |
%(name)s |
用于插入命名的变量值,其中 name 是一个变量名。 |
%(asctime)s |
时间戳,通常带有日期和时间的字符串。asctime 表示 “人类可读的时间”,通常包括日期和时间。 |
%(levelname)s |
日志消息级别,例如 “ERROR”、“INFO” 等。 |
%(message)s |
日志消息的主体部分。 |
最后,logging.error()
这是调用日志记录模块中的 error
方法,表示记录一个错误级别的日志消息,s%
表示字符串格式。通过设置以上格式,最终输出的日志为以下内容,并被记录在myapp.log文件中:
# 519表示毫秒
2023-09-07 13:36:06,519 [ERROR] An error occurred: division by zero
finally
块进行清理操作finally
块,这确保了清理代码始终会执行,即使发生异常也不例外。