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