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

错误消息的最后一行指示发生了什么。 异常有不同的类型,并且类型作为消息的一部分打印:示例中的类型是 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 子句更好,因为它避免了意外捕获未被 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 子句后重新引发该异常。

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

  • 如果 finally 子句执行 breakcontinuereturn 语句,则不会重新引发异常。

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

  • 如果 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.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
    +------------------------------------
>>>