错误处理与调试

What — 是什么

Python 的错误处理基于异常机制——程序在遇到错误时抛出异常对象,沿着调用栈向上传播,直到被 try/except 捕获或导致程序终止。调试则是定位和修复错误的过程,Python 提供了 pdb 调试器、traceback 模块和丰富的日志工具。

核心概念:

  • 异常(Exception):错误事件的对象表示,包含类型、消息和调用栈
  • try/except/else/finally:异常捕获的完整结构
  • 自定义异常:继承 Exception 创建业务语义化的错误类型
  • 调试器(pdb):Python 内置的交互式源码调试器

关键特性:

  • 所有异常都是 BaseException 的子类,用户自定义异常应继承 Exception
  • else 子句在没有异常时执行,finally 子句无论如何都执行
  • 异常链:raise NewError from original_error 保留原始异常上下文
  • Python 3.11+ 引入异常组(ExceptionGroup)和 except* 语法
  • traceback 模块可程序化处理调用栈信息

异常层次结构(常用部分):

BaseException
├── SystemExit                 # sys.exit()
├── KeyboardInterrupt          # Ctrl+C
├── GeneratorExit              # 生成器 close()
└── Exception                  # 所有用户异常的基类
    ├── StopIteration          # 迭代器耗尽
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── ConnectionError
    ├── TypeError              # 类型不匹配
    ├── ValueError             # 值不合法
    ├── AttributeError         # 属性不存在
    ├── ImportError
    │   └── ModuleNotFoundError
    ├── RuntimeError
    │   └── RecursionError
    └── AssertionError         # assert 失败

运行机制:

  • 抛出与传播raise 创建异常对象并中断当前执行流,沿调用栈逐层查找匹配的 except
  • 异常上下文__context__(隐式链)和 __cause__(显式链,raise X from Y
  • traceback 对象__traceback__ 属性保存完整的调用栈帧信息
  • finally 执行时机:在异常传播前执行,可用于资源清理;如果 finally 中 return/raise,会覆盖原始异常

Why — 为什么

适用场景:

  • 输入验证(参数类型/范围检查)
  • IO 操作错误处理(文件、网络、数据库)
  • 业务逻辑错误表达(余额不足、权限不足)
  • 资源清理(文件关闭、连接释放)
  • 防御性编程(对不可控的外部依赖做容错)

错误处理策略对比:

策略方式适用场景
LBYL(Look Before You Leap)if 预检查简单条件、无副作用
EAFP(Easier to Ask Forgiveness than Permission)try/exceptPython 风格、并发安全
返回值None/Result 类型函数式风格、可选结果
日志 + 降级捕获后降级处理生产环境容错

优缺点:

  • ✅ 优点:
    • EAFP 风格避免竞态条件(检查与使用之间的状态变化)
    • 异常自动沿调用栈传播,无需手动传递错误码
    • 业务语义化异常提升代码可读性
    • finally 保证资源清理
  • ❌ 缺点:
    • 异常路径不可见,容易遗漏处理
    • 过度捕获(except Exception)隐藏真实错误
    • 异常对象创建有性能开销(不建议用于正常控制流)
    • 调试时异常链可能很长

How — 怎么用

快速上手

# 完整的 try/except/else/finally 结构
import json

def load_config(path: str) -> dict:
    try:
        with open(path, 'r') as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"配置文件不存在: {path},使用默认配置")
        return {"debug": False}
    except json.JSONDecodeError as e:
        print(f"配置文件格式错误: {e}")
        return {"debug": False}
    else:
        # 没有异常时执行
        print("配置加载成功")
        return data
    finally:
        # 无论如何都执行
        print("配置加载流程结束")

# 自定义异常
class AppError(Exception):
    """应用基础异常"""
    def __init__(self, message: str, code: int = 500):
        self.message = message
        self.code = code
        super().__init__(message)

class NotFoundError(AppError):
    def __init__(self, resource: str):
        super().__init__(f"{resource} 不存在", code=404)

class AuthError(AppError):
    def __init__(self, reason: str = "认证失败"):
        super().__init__(reason, code=401)

raise NotFoundError("用户")

代码示例1:异常链与上下文管理

class DatabaseConnection:
    def __init__(self, url: str):
        self.url = url
        self._conn = None

    def __enter__(self):
        try:
            self._conn = self._connect(self.url)
        except OSError as e:
            # 显式异常链:新异常 from 原始异常
            raise ConnectionError(f"无法连接数据库: {self.url}") from e
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._conn:
            self._conn.close()
        # 不处理异常,让其在调用栈中继续传播
        return False

    def _connect(self, url: str):
        # 模拟连接逻辑
        raise OSError("Connection refused")

    def query(self, sql: str):
        if not self._conn:
            raise RuntimeError("未连接数据库")
        return []

# 使用
try:
    with DatabaseConnection("postgres://localhost/mydb") as db:
        db.query("SELECT * FROM users")
except ConnectionError as e:
    print(f"错误: {e}")
    print(f"原因: {e.__cause__}")  # OSError: Connection refused

# 隐式异常链(在 except 中 raise 新异常时自动关联)
try:
    int("abc")
except ValueError:
    raise RuntimeError("数据处理失败")  # RuntimeError.__context__ = ValueError

代码示例2:Python 3.11+ 异常组与 except*

# ExceptionGroup:一次抛出多个异常
def validate_user(data: dict) -> None:
    errors = []
    if not data.get("name"):
        errors.append(ValueError("姓名不能为空"))
    if not data.get("email"):
        errors.append(ValueError("邮箱不能为空"))
    age = data.get("age", 0)
    if not isinstance(age, int) or age < 0:
        errors.append(TypeError("年龄必须为非负整数"))

    if errors:
        raise ExceptionGroup("用户数据验证失败", errors)

# except* 按类型分别捕获
try:
    validate_user({"name": "", "age": -1})
except* ValueError as eg:
    print(f"值错误 ({len(eg.exceptions)} 个):")
    for e in eg.exceptions:
        print(f"  - {e}")
except* TypeError as eg:
    print(f"类型错误 ({len(eg.exceptions)} 个):")
    for e in eg.exceptions:
        print(f"  - {e}")

# 输出:
# 值错误 (2 个):
#   - 姓名不能为空
#   - 邮箱不能为空
# 类型错误 (1 个):
#   - 年龄必须为非负整数

代码示例3:pdb 调试与 traceback 工具

import pdb
import traceback
import sys

# 方法1:代码中插入断点
def complex_calculation(data: list) -> float:
    total = 0
    for i, item in enumerate(data):
        pdb.set_trace()  # 执行到此处暂停,进入交互调试
        # Python 3.7+ 可用 breakpoint() 代替
        total += item["value"] * item["weight"]
    return total

# 方法2:命令行调试
# python -m pdb myscript.py
# 常用命令:n(next) s(step) c(continue) b(break) p(print) l(list) q(quit)

# 方法3:异常时自动进入调试
def debug_on_exception(func):
    """装饰器:异常时自动进入 pdb"""
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception:
            print("\n--- 异常发生,进入调试模式 ---")
            extype, value, tb = sys.exc_info()
            traceback.print_exc()
            pdb.post_mortem(tb)  # 事后调试
    return wrapper

# traceback 模块:程序化获取调用栈
def format_error(exc: Exception) -> str:
    tb_lines = traceback.format_exception(
        type(exc), exc, exc.__traceback__
    )
    return ''.join(tb_lines)

# 优雅的错误报告
try:
    result = 1 / 0
except ZeroDivisionError as e:
    # 只输出简短信息,不输出完整调用栈
    print(f"计算错误: {e}")
    # 需要完整信息时
    traceback.print_exc()
    # 或者写入日志
    import logging
    logging.error("除零错误", exc_info=True)

常见问题与踩坑

问题原因解决方案
except Exception 吞掉所有错误捕获范围过宽尽量捕获具体异常类型
finally 中 return 覆盖异常finally 中的 return 会吞掉 try 中的异常finally 中不要 return
except: 捕获 SystemExit捕获了 BaseExceptionexcept Exception 代替 except:
异常信息不够详细只打印 str(e)repr(e)traceback.print_exc()
pdb 不生效代码路径被优化跳过不使用 -O 优化标志运行
自定义异常丢失调用栈raise MyError(msg) 未保留原始异常raise MyError(msg) from e 保留异常链

最佳实践

  • 遵循 EAFP 风格:先尝试操作,失败再处理,而非先检查再操作
  • 捕获具体异常,避免 except Exception(最差 except BaseException
  • raise X from Y 保留异常链,不要丢失原始错误信息
  • 自定义异常继承 Exception,构建异常层级(如 AppError → NotFoundError
  • finally 只做资源清理,不要 return 或 raise
  • 生产环境用日志记录异常(logging.exception()),不要 print
  • breakpoint()(Python 3.7+)代替 pdb.set_trace()

面试题

Q1: except 的捕获顺序为什么重要?

Python 按从上到下的顺序匹配 except 子句,第一个匹配的会被执行。如果父类异常(如 Exception)放在子类异常(如 ValueError)前面,子类永远不会被匹配到,因为所有 ValueError 都是 Exception。所以必须把具体(子类)异常放在前面,通用(父类)异常放在后面。

Q2: elsefinally 子句分别在什么时候执行?

else 在 try 块没有抛出任何异常时执行(即正常完成时),用于将可能抛异常的代码和无异常时执行的代码分离,避免意外捕获 else 中的异常。finally 无论是否发生异常、是否 return、是否 raise,都会执行,用于资源清理(关闭文件、释放锁等)。finallyelse 之后执行。

Q3: raise X from Y 和隐式异常链有什么区别?

raise X from Y 是显式异常链,将 Y 设为 X 的 __cause__,表示”X 直接由 Y 引起”,异常信息中显示”直接原因”。隐式异常链发生在 except 块中 raise 新异常时,原异常自动设为新异常的 __context__,表示”处理 Y 时意外发生了 X”。raise X from None 可以抑制异常链,不显示上下文。推荐用显式链(from)使因果关系更清晰。

Q4: 为什么不应该用异常做流程控制?

异常的创建和传播有性能开销(构建 traceback 对象、栈展开),比条件判断慢 10-100 倍。更重要的原因是可读性:异常路径在代码中不可见,阅读者难以追踪哪些代码可能抛出哪些异常。异常应该只用于真正的错误情况(不可预期的失败),而非正常的业务分支(如”用户不存在”这种可预期的结果)。

Q5: Python 3.11 的 ExceptionGroup 解决了什么问题?

之前一次只能抛出一个异常,如果多个并行任务同时失败(如 asyncio.gather),只能抛出第一个异常。ExceptionGroup 允许一次抛出多个异常,并用 except* 语法按类型分别捕获。不同类型的异常可以由不同的处理器处理,未捕获的异常会重新抛出。这对并发场景(多任务、多验证)特别有用。

Q6: pdbstepnext 命令有什么区别?

step(s)进入函数内部,逐行执行被调用的函数。next(n)不进入函数,将函数调用作为单步执行完毕。例如 result = func()step 会进入 func 内部,next 直接执行完 func 并到下一行。until(u)执行到行号大于当前行的位置(用于跳出循环),return(r)执行到当前函数返回。

Q7: 如何自定义异常使其携带更多上下文信息?

继承 Exception 并在 __init__ 中添加自定义属性。最佳实践:1) 提供 message 和错误码等标准字段;2) 用 __str__ 返回用户友好的消息;3) 用 __repr__ 返回调试友好的表示;4) 提供 to_dict() 方法用于 API 序列化。避免在异常中持有大对象引用(可能导致内存泄漏),只存必要的上下文信息。

Q8: sys.exc_info()__traceback__ 属性有什么关系?

sys.exc_info() 返回当前正在处理的异常三元组 (type, value, traceback),只在 except 块内有意义。异常对象的 __traceback__ 属性保存该异常的调用栈信息。sys.exc_info()[2]exc.__traceback__ 指向同一个 traceback 对象。traceback.format_exception(*sys.exc_info()) 可以格式化完整的异常信息。Python 3.10+ 推荐用 traceback.format_exception(exc) 直接传入异常对象。


相关链接: