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>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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>
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>
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>
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>
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>
  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>
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>
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>
KeyboardInterrupt

如果存在 finally 子句,则 finally 子句将在 try 语句完成之前作为最后一个任务执行。无论 try 语句是否产生异常,finally 子句都会运行。以下几点讨论了异常发生时的更复杂情况

  • 如果在执行 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>
  File "<stdin>", line 3, in divide
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>
  |   File "<stdin>", line 3, in f
  | ExceptionGroup: there were problems
  +-+---------------- 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>
  |   File "<stdin>", line 2, in f
  | ExceptionGroup: group1
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2
    +-+---------------- 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>
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>
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>