8. 错误和异常

至此,本教程还未深入介绍错误信息,但如果你尝试过本教程前文的例子,应该已经看到过一些错误信息。
错误可(至少)被分为两种:语法错误异常

8.1. 语法错误

语法错误(解析错误)是学习 Python 时最常见的错误,例如:

while True print('Hello world')

解释器会重复出错的行并显示指向检测到错误的位置的小箭头,并打印文件名和行号。
例如:

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

注意
箭头指示的位置不一定就是需要修复的位置(此例中实际是缺少冒号 :)。

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

错误信息的最后一行说明了异常类型(如 ZeroDivisionErrorNameErrorTypeError)。
标准异常类型是内置标识符(非关键字)。
未处理异常的 __str__() 输出会被打印为异常消息的最后部分(“detail”)。
内置异常基类为 BaseException,大多数非致命异常继承自 Exception

8.3. 异常的处理

可以编写程序处理选定的异常。
例如,反复要求用户输入直到输入有效整数:

while True:
    try:
        x = int(input("Please enter a number: "))
        break
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

try 语句工作流程概述:

  • 执行 try 子句内容。
  • 若无异常,跳过 except,继续执行。
  • 若发生异常,跳过 try 中剩余部分,尝试与 except 指定的异常匹配,
    匹配则执行该 except,然后继续执行 try/except 之后的代码。
  • 未匹配的异常将向外层传播,最终成为未处理异常导致程序停止。

可以有多个 except 子句(最多执行一个)。
可以用元组在同一 except 中列出多个异常:

except (RuntimeError, TypeError, NameError):
    pass

异常匹配遵循继承关系;
捕获顺序重要(先匹配的先处理)。

异常可以带参数;except Exception as inst: 可以将异常实例绑定到变量 inst,并读取 inst.args
示例:

try:
    raise Exception('spam', 'eggs')
except Exception as inst:
    print(type(inst))
    print(inst.args)
    print(inst)
    x, y = inst.args
    print('x =', x)
    print('y =', y)

常见最佳实践:尽量具体地捕获异常,必要时记录并重新抛出。
示例:

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 子句,当 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()

异常处理也会覆盖在 try 中调用(或间接调用)的函数中发生的异常,例如:

def this_fails():
    x = 1/0

try:
    this_fails()
except ZeroDivisionError as err:
    print('Handling run-time error:', err)

8.4. 触发异常

使用 raise 强制触发指定异常:

raise NameError('HiThere')

输出:

Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
        raise NameError('HiThere')
NameError: HiThere

raise 的参数必须是异常实例或异常类(派生自 BaseException)。
如果只想重新抛出当前异常,可用不带参数的 raise

try:
    raise NameError('HiThere')
except NameError:
    print('An exception flew by!')
    raise

8.5. 异常链

如果在 except 中发生未处理异常,错误信息会显示被处理异常的上下文。
可以用 from 指示直接原因:

# exc 必须为异常实例或 None
raise RuntimeError from exc

示例将一个 ConnectionError 转换为 RuntimeError

def func():
    raise ConnectionError

try:
    func()
except ConnectionError as exc:
    raise RuntimeError('Failed to open database') from exc

也可以用 from None 禁用自动异常链:

try:
    open('database.sqlite')
except OSError:
    raise RuntimeError from None

8.6. 用户自定义异常

自定义异常应从 Exception 派生。
异常类可以包含属性以便处理器提取信息。
大多数异常名以 “Error” 结尾。

8.7. 定义清理操作

try 还有 finally 子句,用于定义无论是否发生异常都要执行的清理操作:

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 中发生异常且未被 except 处理,finally 执行后异常会重新抛出。
  • exceptelse 中发生异常也会在 finally 执行后重新抛出。
  • finally 中执行 breakcontinuereturn,则异常不会重新抛出(从 Python 3.14 开始对此会发出 SyntaxWarning)。
  • finally 中的 return 会覆盖 try 中的返回值(不推荐,3.14 起有警告)。

示例:

def bool_return():
    try:
        return True
    finally:
        return False

print(bool_return())  # False

另一个例子说明 exceptelsefinally 的交互:

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)
divide(2, 0)
divide("2", "1")

输出片段:

result is 2.0
executing finally clause
division by zero!
executing finally clause
executing finally clause
Traceback (most recent call last):
    ...
TypeError: unsupported operand type(s) for /: 'str' and 'str'

finally 常用于释放外部资源(文件、网络连接等)。

8.8. 预定义的清理操作

某些对象定义了预定义清理操作,例如文件对象。
与直接使用 open() 迭代相比,with 语句能确保及时正确的清理:

for line in open("myfile.txt"):
    print(line, end="")

此写法可能导致文件较长时间未关闭,推荐使用:

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

with 语句在语句结束后总会关闭文件(即使处理中出现异常)。

8.9. 引发和处理多个不相关的异常

在并发或需要收集多个错误的情况下,可使用 ExceptionGroup 打包多个异常并一起引发:

def f():
    excs = [OSError('error 1'), SystemError('error 2')]
    raise ExceptionGroup('there were problems', excs)
    
f()

示例输出(简化)将显示 ExceptionGroup 及其子异常。
使用 except* 可以选择性地处理异常组中匹配的异常类型:

def f():
    excs = [OSError('error 1'), SystemError('error 2')]
    raise ExceptionGroup('there were problems', excs)

try:
    f()
except* OSError as e:
    print("There were OSErrors")
except* SystemError as e:
    print("There were SystemErrors")

注意
嵌套在异常组中的异常必须是实例(通常是已被提出并捕获的异常实例)。

示例收集多个测试错误后抛出组:

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

更实用的用法:当把异常收集到异常组中时,为每个异常添加上下文说明:

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)

输出将显示每个子异常及其对应的注释(例如 “Happened in Iteration 1/2/3”)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐