到目前为止,本教程只是提到过错误信息。如果你尝试了前面的示例,大概已经见过一些错误。错误至少可以分为两类:语法错误和异常。
8.1. 语法错误
语法错误也叫解析错误,是刚开始学习 Python 时最常见的错误之一:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
解析器会重复显示出错的行,并用小箭头指出检测到错误的位置。这个位置不一定就是需要修改的位置。上例在 print() 处检测到错误,实际原因是它前面缺少冒号 :。
错误信息还会打印文件名(上例为 <stdin>)和行号;当输入来自文件时,这些信息能帮助你找到出错的位置。
8.2. 异常
语句或表达式即使语法正确,执行时仍可能出错。执行期间检测到的错误称为异常。异常并不一定致命,接下来会介绍如何在程序中处理它们。不过,许多异常没有被程序处理,因而产生下面这样的错误信息:
>>> 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 语句按以下方式工作:
- 首先执行
try子句,也就是try与except关键字之间的语句。 - 如果没有异常,跳过
except子句,完成这条try语句。 - 如果执行
try子句时发生异常,跳过该子句剩下的部分。若异常类型与except后指定的类型匹配,执行对应的处理器,随后继续执行整个try/except块之后的代码。 - 若类型不匹配,异常传递到外层
try语句。找不到处理器时,异常未被处理,程序停止并输出错误信息。
一条 try 语句可以有多个 except 子句,分别处理不同异常,但最多执行一个处理器。处理器只处理对应 try 子句内发生的异常,不处理同一条 try 语句的其他处理器中发生的异常。一个 except 子句也可以列出多个异常:
... except RuntimeError, TypeError, NameError:
... pass
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 可以在异常名称之后指定变量,将它绑定到异常实例。实例通常有一个 args 属性保存参数。为方便使用,内置异常定义了 __str__(),因此不用显式访问 .args 也能打印这些参数。
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # 异常的类型
... print(inst.args) # 参数保存在 .args 中
... print(inst) # __str__ 允许 args 被直接打印,
... # 但可能在异常子类中被覆盖
... x, y = inst.args # 解包 args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
对于未处理的异常,__str__() 的输出就是错误消息最后的详细说明。
BaseException 是所有异常的共同基类。其中的 Exception 子类是所有非致命异常的基类。不是 Exception 子类的异常通常不应被捕获,因为它们用来表示程序应当终止,例如 sys.exit() 引发的 SystemExit,以及用户中断程序时引发的 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 内的异常,也处理在 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 # '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 必须为异常实例或为 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. 用户自定义异常
程序可以定义新的异常类,为自己的异常命名。关于类的更多内容见类一章。异常类通常应当直接或间接继承 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,它会在 try 语句完成之前最后执行。无论 try 是否产生异常,都将执行它。更复杂的情况包括:
- 在
try中产生的异常可以由except处理;若没有被处理,会在执行finally后重新抛出。 except或else内产生的异常,也会在执行finally后重新抛出。- 若
finally执行break、continue或return,异常不会重新抛出。这容易造成混淆,因此不推荐。从 Python 3.14 起,编译器会对此发出SyntaxWarning,参见 PEP 765。 - 如果
try到达break、continue或return,会先执行finally,再执行相应的跳转或返回。 - 如果
finally中包含return,最终返回的是该子句返回的值,而非try的返回值。这也容易令人混淆,因而不推荐。从 Python 3.14 起编译器同样会发出SyntaxWarning。
例如:
>>> 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.9. 引发和处理多个不相关的异常
有时需要报告已经发生的多个异常。例如,在并发框架中,多个任务可能同时失败;其他场景也可能需要继续执行并收集错误,而不是遇到第一个异常就停止。
内置的 ExceptionGroup 把一组异常实例包装起来,使它们可以一起抛出。它本身也是异常,因此可以像其他异常一样捕获:
>>> def f():
... excs = [OSError('error 1'), SystemError('error 2')]
... raise ExceptionGroup('there were problems', excs)
...
>>> f()
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| f()
| ~^^
| File "<stdin>", line 3, in f
| raise ExceptionGroup('there were problems', excs)
| ExceptionGroup: there were problems (2 sub-exceptions)
+-+---------------- 1 ----------------
| OSError: error 1
+---------------- 2 ----------------
| SystemError: error 2
+------------------------------------
>>> try:
... f()
... except Exception as e:
... print(f'caught {type(e)}: {e}')
...
caught <class 'ExceptionGroup'>: there were problems (2 sub-exceptions)
>>>
用 except* 代替 except,可以只处理异常组中匹配指定类型的异常。下面的嵌套异常组中,每个 except* 都提取相应类型的异常,让其余异常继续传递到其他子句,最终仍未处理的异常会重新抛出。
>>> def f():
... raise ExceptionGroup(
... "group1",
... [
... OSError(1),
... SystemError(2),
... ExceptionGroup(
... "group2",
... [
... OSError(3),
... RecursionError(4)
... ]
... )
... ]
... )
...
>>> try:
... f()
... except* OSError as e:
... print("There were OSErrors")
... except* SystemError as e:
... print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise ExceptionGroup(
| ...<12 lines>...
| )
| ExceptionGroup: group1 (1 sub-exception)
+-+---------------- 1 ----------------
| ExceptionGroup: group2 (1 sub-exception)
+-+---------------- 1 ----------------
| RecursionError: 4
+------------------------------------
>>>
异常组中嵌套的异常必须是实例,不能是类型。实际程序通常会收集已经抛出并捕获的异常,模式如下:
>>> excs = []
... for test in tests:
... try:
... test.run()
... except Exception as e:
... excs.append(e)
...
>>> if excs:
... raise ExceptionGroup("Test Failures", excs)
...
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
+------------------------------------
>>>











暂无评论内容