8. 错误和异常

到目前为止,我们仅仅提到了错误消息,但是如果你尝试过这些示例,你可能已经遇到过一些。错误至少有两种可区分的类型:语法错误异常

8.1. 语法错误

语法错误,也称为解析错误,可能是你还在学习 Python 时遇到的最常见的抱怨。

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

解析器会重复显示出错的行,并用小箭头指向检测到错误的位置。请注意,这不总是需要修复的地方。在这个例子中,错误是在 print() 函数处检测到的,因为它前面缺少一个冒号(':')。

文件名称(我们示例中的 <stdin>)和行号会被打印出来,这样如果输入来自文件,你就知道在哪里查找。

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

错误消息的最后一行指示发生了什么。异常有不同的类型,并且类型作为消息的一部分打印出来:示例中的类型是 ZeroDivisionErrorNameErrorTypeError。作为异常类型打印的字符串是发生的内置异常的名称。所有内置异常都是如此,但对于用户定义的异常则不一定(尽管这是一个有用的约定)。标准异常名称是内置标识符(不是保留关键字)。

行的其余部分根据异常类型和导致异常的原因提供详细信息。

错误消息的前一部分以堆栈回溯的形式显示了异常发生的上下文。通常,它包含列出源行的堆栈回溯;但是,它不会显示从标准输入读取的行。

内置异常 列出了内置异常及其含义。

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 子句tryexcept 关键字之间的语句)。

  • 如果没有发生异常,则跳过 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

tryexcept 语句有一个可选的 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 子句中添加额外代码更好,因为它避免了意外捕获并非由受 tryexcept 语句保护的代码引发的异常。

异常处理程序不仅处理在 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 子句执行后重新引发异常。

  • 异常可能发生在执行 exceptelse 子句期间。同样,在 finally 子句执行后重新引发异常。

  • 如果 finally 子句执行 breakcontinuereturn 语句,则不会重新引发异常。这可能会令人困惑,因此不鼓励这样做。从 3.14 版本开始,编译器会为此发出 SyntaxWarning(请参阅 PEP 765)。

  • 如果 try 语句到达 breakcontinuereturn 语句,则 finally 子句将在 breakcontinuereturn 语句执行之前执行。

  • 如果 finally 子句包含一个 return 语句,则返回的值将是来自 finally 子句的 return 语句的值,而不是来自 try 子句的 return 语句的值。这可能会令人困惑,因此不鼓励这样做。从 3.14 版本开始,编译器会为此发出 SyntaxWarning(请参阅 PEP 765)。

例如:

>>> 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'>: e
>>>

通过使用 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
    +------------------------------------
>>>