文章
合集Python 语言基础第 14 / 21 篇

Python 异常处理词典笔记

提示

来源:Python 3.14 官方文档教程 - 错误和异常章节 只做词典类用途,业务使用细节见本目录中的其他笔记

8.1. 语法错误(SyntaxError)

语法错误又称解析错误,是学 Python 最常遇到的错误。

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

特点:

  • 解析器会重复出错的行并显示指向检测到错误的位置的小箭头
  • 错误位置不一定是需要被修复的位置(上例中缺少冒号导致 print() 被标记)
  • 会打印文件名和行号以便定位

8.2. 异常(Exception)

语法正确不代表运行时不会出错。执行时检测到的错误叫异常。

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

异常继承关系:

BaseException
├── SystemExit          # sys.exit() 引发
├── KeyboardInterrupt   # 用户中断(Ctrl+C)
└── Exception           # 所有非致命异常的基类
    ├── ArithmeticError
    │   └── ZeroDivisionError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── TypeError
    ├── ValueError
    └── ...

8.3. 异常的处理(try/except)

基本语法

try:
    # 可能引发异常的代码
    x = int(input("请输入一个数字: "))
except ValueError:
    # 处理特定异常
    print("这不是一个有效的数字!")

执行流程:

  1. 首先执行 try 子句(try 和 except 之间的语句)
  2. 如果没有触发异常,跳过 except 子句,try 语句执行完毕
  3. 如果发生异常,跳过 try 子句剩余部分
  4. 如果异常类型与 except 匹配,执行 except 子句
  5. 如果异常类型不匹配,传递给外层 try 语句;如果没有处理器,程序终止

捕获多个异常

# 方式1:多个 except 子句
try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print(f"OS 错误: {err}")
except ValueError:
    print("无法转换为整数")
except Exception as err:
    print(f"意外错误: {err=}, {type(err)=}")
    raise  # 重新抛出

# 方式2:元组指定多个异常
except (RuntimeError, TypeError, NameError):
    pass

异常类的匹配规则

  • except 子句中的类匹配该类本身或其派生类的实例
  • 顺序很重要!派生类在前会拦截基类异常
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")
# 输出: B, C, D

# 如果颠倒顺序(B 放最前),则全部输出 B

获取异常详细信息

try:
    raise Exception('参数1', '参数2')
except Exception as inst:
    print(type(inst))     # <class 'Exception'>
    print(inst.args)      # ('参数1', '参数2')
    print(inst)           # ('参数1', '参数2')
    x, y = inst.args      # 解包
    print(f'x = {x}')     # x = 参数1
    print(f'y = {y}')     # y = 参数2

else 子句

try 语句可以有可选的 else 子句,必须放在所有 except 之后。

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('无法打开', arg)
    else:
        print(arg, '有', len(f.readlines()), '行')
        f.close()

优点: 避免意外捕获非 try 子句保护的代码触发的异常。

异常处理的传播

异常处理程序不仅处理 try 子句中的异常,还处理 try 子句中调用的函数里的异常:

def this_fails():
    x = 1/0

try:
    this_fails()  # 函数内部触发异常
except ZeroDivisionError as err:
    print('处理运行时错误:', err)

8.4. 触发异常(raise)

基本用法

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere
  • 参数必须是异常实例或异常类
  • 如果是异常类,会隐式调用无参构造函数实例化
raise ValueError        # 简写,等价于 raise ValueError()
raise ValueError()      # 完整写法
raise ValueError('无效值')  # 带参数的实例

重新抛出异常

try:
    raise NameError('HiThere')
except NameError:
    print('一个异常飞过了!')
    raise  # 重新触发当前异常

8.5. 异常链(Exception Chaining)

隐式异常链

如果在 except 块中发生了新的异常,新异常会被附加到原异常上:

try:
    open("database.sqlite")
except OSError:
    raise RuntimeError("unable to handle error")

# 输出:
# FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
# During handling of the above exception, another exception occurred:
# RuntimeError: unable to handle error

显式异常链(raise ... from ...)

def func():
    raise ConnectionError

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

# 输出:
# The above exception was the direct cause of the following exception:
# RuntimeError: Failed to open database

禁用异常链

try:
    open('database.sqlite')
except OSError:
    raise RuntimeError from None  # 不显示原始异常

8.6. 用户自定义异常

  • 自定义异常应从 Exception 类派生(直接或间接)
  • 异常类可以包含任意属性
  • 命名通常以 "Error" 结尾
class ValidationError(Exception):
    """数据验证失败时抛出"""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"字段 '{field}': {message}")

# 使用
try:
    if not email:
        raise ValidationError('email', '不能为空')
except ValidationError as e:
    print(f"验证失败: {e}")
    print(f"问题字段: {e.field}")

8.7. 定义清理操作(finally)

finally 子句定义在任何情况下都必须执行的清理操作。

try:
    raise KeyboardInterrupt
finally:
    print('Goodbye, world!')

# 输出:
# Goodbye, world!
# KeyboardInterrupt 被重新触发

执行规则:

  1. 无论是否发生异常,finally 都会执行
  2. 如果发生异常但没有匹配的 except,finally 执行后异常重新触发
  3. finally 中的 return/break/continue 会抑制异常的重新触发(⚠️ 不推荐)
  4. 如果 try 中有 return,finally 会在返回前执行
def divide(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("除零错误!")
    else:
        print(f"结果是 {result}")
    finally:
        print("执行 finally 子句")

divide(2, 1)   # 结果: 2.0, finally
divide(2, 0)   # 除零错误!, finally
divide("2", "1")  # finally, 然后 TypeError

典型用途: 释放外部资源(文件、网络连接等)

8.8. 预定义的清理操作(with 语句)

有些对象定义了标准的清理操作,无论是否成功都会执行。

问题代码:

for line in open("myfile.txt"):
    print(line, end="")
# 文件在不确定的时间内保持打开

正确做法:

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")
# 语句执行完毕后,即使发生异常,文件 f 也会被关闭

with 语句是上下文管理器的语法糖,确保资源及时关闭。

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

Python 3.11+ 引入 ExceptionGroup,可以打包多个异常一起引发。

基本用法

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

f()
# 输出:
# ExceptionGroup: there were problems (2 sub-exceptions)
# +---------------- 1 ----------------
# | OSError: error 1
# +---------------- 2 ----------------
# | SystemError: error 2

选择性捕获(except*)

except* 只处理组中特定类型的异常:

def f():
    raise ExceptionGroup(
        "group1",
        [
            OSError(1),
            SystemError(2),
            ExceptionGroup(
                "group2",
                [OSError(3), RecursionError(4)]
            )
        ]
    )

try:
    f()
except* OSError as e:
    print("有 OSError")
except* SystemError as e:
    print("有 SystemError")
# RecursionError 未被捕获,继续传播

收集多个异常的典型模式

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)

异常有 add_note(note) 方法,可以加字符串注释,在回溯中显示。

基本用法

try:
    raise TypeError('bad type')
except Exception as e:
    e.add_note('添加一些信息')
    e.add_note('添加更多信息')
    raise

# 输出:
# 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)

# 每个异常都有注释说明发生的迭代次数

异常处理最佳实践

  1. 具体捕获:尽量捕获具体的异常类型,不要滥用 except Exception
  2. 不要吞没异常:捕获后至少记录,必要时重新抛出
  3. 使用上下文管理器:with 语句自动管理资源
  4. 合理使用 else:将正常逻辑与异常处理分离
  5. 用好 finally:确保清理操作被执行
  6. 异常链:转换异常时用 raise ... from ... 保留上下文
  7. 自定义异常:为业务逻辑定义清晰的异常层次结构

速查表

# 基本结构
try:
    # 尝试执行
    pass
except SpecificError as e:
    # 处理特定异常
    pass
except (Error1, Error2) as e:
    # 处理多个异常
    pass
except Exception as e:
    # 兜底处理
    raise
else:
    # 无异常时执行
    pass
finally:
    # 总是执行
    pass

# 抛出异常
raise ValueError("message")
raise  # 重新抛出当前异常
raise NewError() from old_exc  # 异常链

# 上下文管理器
with open("file") as f:
    pass  # 自动关闭

# 异常组(3.11+)
raise ExceptionGroup("msg", [exc1, exc2])
except* SpecificError as e:
    pass  # 只捕获特定类型

# 添加注释
try:
    raise Error()
except Error as e:
    e.add_note("额外信息")
    raise