错误处理与调试
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/except | Python 风格、并发安全 |
| 返回值 | 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 | 捕获了 BaseException | 用 except 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: else 和 finally 子句分别在什么时候执行?
else在 try 块没有抛出任何异常时执行(即正常完成时),用于将可能抛异常的代码和无异常时执行的代码分离,避免意外捕获 else 中的异常。finally无论是否发生异常、是否 return、是否 raise,都会执行,用于资源清理(关闭文件、释放锁等)。finally在else之后执行。
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: pdb 的 step 和 next 命令有什么区别?
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)直接传入异常对象。
相关链接:
- GIL与并发模型
- 生成器与迭代器
- 包管理与虚拟环境
- Python 异常文档