错误和异常:Python 官方教程

到目前为止,本教程只是提到过错误信息。如果你尝试了前面的示例,大概已经见过一些错误。错误至少可以分为两类:语法错误和异常。

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

来源:Python Software Foundation 与 Python 文档、简体中文翻译贡献者,英文原文及官方简体中文版本。核验版本为 Python 3.14.8;控制台回溯是原文示例输出,并非本次实测结果。

Copyright © 2001 Python Software Foundation; All Rights Reserved。正文采用 Python Software Foundation License Version 2;文档中的示例代码还采用 Zero-Clause BSD License。改动摘要:依据官方中文和英文整理中文措辞、修正中文页面中的重复词,调整网页标记与链接;保留完整十节教学内容及代码、输出。代码中的注释保留官方中文版本。

Python Software Foundation License Version 2
1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and
   the Individual or Organization ("Licensee") accessing and otherwise using this
   software ("Python") in source or binary form and its associated documentation.

2. Subject to the terms and conditions of this License Agreement, PSF hereby
   grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
   analyze, test, perform and/or display publicly, prepare derivative works,
   distribute, and otherwise use Python alone or in any derivative
   version, provided, however, that PSF's License Agreement and PSF's notice of
   copyright, i.e., "Copyright © 2001 Python Software Foundation; All Rights
   Reserved" are retained in Python alone or in any derivative version
   prepared by Licensee.

3. In the event Licensee prepares a derivative work that is based on or
   incorporates Python or any part thereof, and wants to make the
   derivative work available to others as provided herein, then Licensee hereby
   agrees to include in any such work a brief summary of the changes made to Python.

4. PSF is making Python available to Licensee on an "AS IS" basis.
   PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED.  BY WAY OF
   EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR
   WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE
   USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.

5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
   FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
   MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE
   THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.

6. This License Agreement will automatically terminate upon a material breach of
   its terms and conditions.

7. Nothing in this License Agreement shall be deemed to create any relationship
   of agency, partnership, or joint venture between PSF and Licensee.  This License
   Agreement does not grant permission to use PSF trademarks or trade name in a
   trademark sense to endorse or promote products or services of Licensee, or any
   third party.

8. By copying, installing or otherwise using Python, Licensee agrees
   to be bound by the terms and conditions of this License Agreement.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容