8. 错误和异常¶
到目前为止,错误消息只是被提及,但如果你尝试过这些示例,你可能已经看到了一些。 错误(至少)有两种明显的类型:语法错误和异常。
8.1. 语法错误¶
语法错误,也称为解析错误,可能是你学习 Python 时最常见的抱怨
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
解析器重复出错的行,并显示小箭头,指向在该行中检测到错误的标记。 该错误可能是由指示的标记之前缺少标记引起的。 在示例中,错误是在函数 print() 处检测到的,因为它之前缺少冒号 (':')。 打印文件名和行号,以便你知道在输入来自脚本时应该在哪里查找。
8.2. 异常¶
即使语句或表达式在语法上正确,尝试执行它时也可能会导致错误。 执行期间检测到的错误称为异常,并且不是无条件致命的:你将很快学习如何在 Python 程序中处理它们。 但是,大多数异常不会被程序处理,并且会导致此处所示的错误消息
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
4 + spam*3
^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'2' + 2
~~~~^~~
TypeError: can only concatenate str (not "int") to str
错误消息的最后一行指示发生了什么。 异常有不同的类型,并且类型作为消息的一部分打印:示例中的类型是 ZeroDivisionError、 NameError 和 TypeError。 作为异常类型打印的字符串是发生的内置异常的名称。 这对于所有内置异常都是如此,但对于用户定义的异常可能不适用(尽管这是一个有用的约定)。 标准异常名称是内置标识符(而不是保留关键字)。
该行的其余部分根据异常的类型和导致异常的原因提供了详细信息。
错误消息的前面部分以堆栈回溯的形式显示异常发生的上下文。 通常,它包含列出源代码行的堆栈回溯; 但是,它不会显示从标准输入读取的行。
内置异常 列出了内置异常及其含义。
8.3. 处理异常¶
可以编写处理选定异常的程序。 看一下下面的示例,该示例要求用户输入直到输入有效的整数,但允许用户中断程序(使用 Control-C 或操作系统支持的任何内容); 请注意,用户生成的中断通过引发 KeyboardInterrupt 异常来发出信号。
>>> while True:
... try:
... x = int(input("Please enter a number: "))
... break
... except ValueError:
... print("Oops! That was no valid number. Try again...")
...
try 语句的工作方式如下。
如果未发生异常,则跳过 except 子句,并且
try语句的执行完成。如果在执行
try子句期间发生异常,则跳过该子句的其余部分。 然后,如果其类型与except关键字后命名的异常匹配,则执行 except 子句,然后在 try/except 块之后继续执行。如果发生的异常与 except 子句中命名的异常不匹配,则将其传递给外部
try语句; 如果找不到处理程序,则它是一个未处理的异常,并且执行会停止并显示错误消息。
try 语句可以有多个 except 子句,以指定不同异常的处理程序。 最多执行一个处理程序。 处理程序仅处理在相应的 try 子句中发生的异常,而不处理同一 try 语句中的其他处理程序。 except 子句可以将多个异常命名为带括号的元组,例如
... except (RuntimeError, TypeError, NameError):
... pass
except 子句中的类匹配作为该类本身或其派生类之一的实例的异常(但反之亦然——列出派生类的 except 子句不匹配其基类的实例)。 例如,以下代码将按顺序打印 B、C、D
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")
请注意,如果 except 子句被反转(首先是 except B),它将打印 B、B、B——第一个匹配的 except 子句被触发。
当发生异常时,它可能具有关联的值,也称为异常的参数。 参数的存在和类型取决于异常类型。
except 子句可以在异常名称后指定一个变量。 该变量绑定到通常具有存储参数的 args 属性的异常实例。 为了方便起见,内置异常类型定义 __str__() 以打印所有参数,而无需显式访问 .args。
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # the exception type
... print(inst.args) # arguments stored in .args
... print(inst) # __str__ allows args to be printed directly,
... # but may be overridden in exception subclasses
... x, y = inst.args # unpack args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
异常的 __str__() 输出作为未处理异常的消息的最后一部分(“详细信息”)打印。
BaseException 是所有异常的公共基类。 它的子类之一 Exception 是所有非致命异常的基类。 通常不处理不是 Exception 子类的异常,因为它们用于指示程序应终止。 它们包括 SystemExit,它由 sys.exit() 引发,以及 KeyboardInterrupt,当用户希望中断程序时引发。
Exception 可以用作捕获(几乎)所有内容的通配符。 但是,最佳做法是尽可能具体地说明我们打算处理的异常类型,并允许任何意外的异常继续传播。
处理 Exception 的最常见模式是打印或记录异常,然后重新引发它(允许调用者也处理该异常)
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error:", err)
except ValueError:
print("Could not convert data to an integer.")
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
try … except 语句有一个可选的 else 子句,该子句如果存在,则必须位于所有 except 子句之后。 它对于如果 try 子句未引发异常时必须执行的代码很有用。 例如
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
使用 else 子句比将额外的代码添加到 try 子句更好,因为它避免了意外捕获未被 try ... except 语句保护的代码引发的异常。
异常处理程序不仅处理立即在 try 子句中发生的异常,还处理在 try 子句中调用(甚至间接调用)的函数内部发生的异常。 例如
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('Handling run-time error:', err)
...
Handling run-time error: division by zero
8.4. 抛出异常¶
raise 语句允许程序员强制发生指定的异常。 例如
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('HiThere')
NameError: HiThere
raise 的唯一参数表示要引发的异常。这必须是异常实例或异常类(派生自 BaseException 的类,例如 Exception 或其子类之一)。如果传递的是异常类,它将通过调用其不带参数的构造函数来隐式实例化。
raise ValueError # shorthand for 'raise ValueError()'
如果您需要确定是否引发了异常,但不打算处理它,则可以使用 raise 语句的更简单形式来重新引发异常。
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('An exception flew by!')
... raise
...
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('HiThere')
NameError: HiThere
8.5. 异常链¶
如果在 except 部分内发生未处理的异常,它将附加正在处理的异常,并包含在错误消息中。
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("database.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error
为了指示异常是另一个异常的直接结果,raise 语句允许使用可选的 from 子句。
# exc must be exception instance or None.
raise RuntimeError from exc
当您转换异常时,这非常有用。例如
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
func()
~~~~^^
File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database
它还允许使用 from None 惯用法禁用自动异常链接。
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
有关链接机制的更多信息,请参阅内置异常。
8.6. 用户定义的异常¶
程序可以通过创建新的异常类来命名自己的异常(有关 Python 类的更多信息,请参阅类)。异常通常应直接或间接地从 Exception 类派生。
可以定义异常类,它们可以执行任何其他类可以执行的任何操作,但通常保持简单,通常仅提供一些属性,允许异常的处理程序提取有关错误的信息。
大多数异常的定义名称都以“Error”结尾,类似于标准异常的命名方式。
许多标准模块定义了自己的异常,以报告它们定义的函数中可能发生的错误。
8.7. 定义清理操作¶
try 语句具有另一个可选子句,旨在定义在任何情况下都必须执行的清理操作。例如
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
如果存在 finally 子句,则 finally 子句将在 try 语句完成之前作为最后一项任务执行。finally 子句无论 try 语句是否产生异常都会运行。以下几点讨论了发生异常时更复杂的情况
如果在执行
try子句期间发生异常,则该异常可能由except子句处理。如果异常未被except子句处理,则在执行finally子句后重新引发该异常。在执行
except或else子句期间可能会发生异常。同样,在执行finally子句后重新引发该异常。如果
try语句遇到break、continue或return语句,则finally子句将在break、continue或return语句执行之前立即执行。如果
finally子句包含return语句,则返回的值将是finally子句的return语句中的值,而不是try子句的return语句中的值。
例如
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
一个更复杂的例子
>>> 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 "<stdin>", line 1, in <module>
divide("2", "1")
~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in divide
result = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
如您所见,finally 子句在任何情况下都会执行。由两个字符串相除引发的 TypeError 不会被 except 子句处理,因此在执行 finally 子句后重新引发。
在实际应用中,无论资源的使用是否成功,finally 子句都可用于释放外部资源(如文件或网络连接)。
8.8. 预定义的清理操作¶
一些对象定义了当不再需要该对象时要执行的标准清理操作,无论使用该对象的操作是否成功。请看下面的示例,该示例尝试打开一个文件并将其内容打印到屏幕上。
for line in open("myfile.txt"):
print(line, end="")
此代码的问题在于,它会在代码的这部分完成执行后,将文件打开不确定的时间。这在简单的脚本中不是问题,但对于较大的应用程序来说可能会成为问题。with 语句允许以确保及时且正确地清理文件的方式使用文件等对象。
with open("myfile.txt") as f:
for line in f:
print(line, end="")
执行该语句后,无论在处理行时是否遇到问题,文件f始终会关闭。像文件一样提供预定义的清理操作的对象将在其文档中指出这一点。
8.10. 用注释丰富异常¶
创建异常以引发时,通常会使用描述已发生错误的信息对其进行初始化。在某些情况下,在捕获异常后添加信息非常有用。为此,异常有一个方法 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
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>
例如,当将异常收集到异常组中时,我们可能想为各个错误添加上下文信息。在以下示例中,组中的每个异常都包含一个指示此错误发生时间的注释。
>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Happened in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('We have some problems', excs)
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>