02-异常处理
Python 版本要求: 本教程基于 Python 3.11+ 编写,部分特性需要 3.11 或更高版本。
导航: 错误与异常基础 → 异常处理 → 抛出异常 → 上下文管理器
概念铺垫
第一步:实际场景引入
为什么需要异常处理?
假设你编写一个用户注册功能:
python
def register(username: str, age: str, email: str) -> dict[str, str | int]:
"""用户注册"""
# 转换年龄
age_int: int = int(age) # 用户输入 "abc" 怎么办?
# 保存到数据库
save_to_database(username, age_int, email) # 数据库连接失败怎么办?
return {"username": username, "age": age_int, "email": email}问题来了:
场景 1: 用户年龄输入 "二十岁"
→ int("二十岁") 抛出 ValueError
→ 程序崩溃 💥
场景 2: 数据库连接断开
→ save_to_database() 抛出 ConnectionError
→ 程序崩溃 💥
场景 3: 用户邮箱格式错误
→ 需要验证并提示用户
→ 如何优雅地处理?思考: 如何让程序在遇到错误时继续运行,并给用户友好的提示?
第二步:概念动机
异常处理的核心思想
┌─────────────────────────────────────────┐
│ 异常处理的核心思想 │
├─────────────────────────────────────────┤
│ │
│ 传统方式(检查返回值): │
│ ───────────────────────── │
│ result = do_something() │
│ if result == ERROR_1: │
│ handle_error_1() │
│ elif result == ERROR_2: │
│ handle_error_2() │
│ ... │
│ │
│ 问题: │
│ • 代码冗长,难以阅读 │
│ • 容易忘记检查错误 │
│ • 错误处理与业务逻辑混在一起 │
│ │
│ ───────────────────────────────── │
│ │
│ 异常处理方式: │
│ ───────────────────────── │
│ try: │
│ result = do_something() │
│ except Error1: │
│ handle_error_1() │
│ except Error2: │
│ handle_error_2() │
│ │
│ 优势: │
│ • 错误处理与业务逻辑分离 │
│ • 不会忘记检查(异常会传播) │
│ • 代码更清晰易读 │
│ │
└─────────────────────────────────────────┘核心理解:
- try:包裹可能出错的代码
- except:捕获并处理异常
- else:没有异常时执行
- finally:无论如何都执行
L1 理解层:会用
第三步:最简示例
基本的 try-except
python
def safe_divide(a: int, b: int) -> float | None:
"""安全除法"""
try:
return a / b
except ZeroDivisionError:
print("错误:除数不能为零")
return None
# 使用
result1: float | None = safe_divide(10, 2) # 5.0
result2: float | None = safe_divide(10, 0) # None,打印错误信息第四步:详细讲解
try-except 语句
基本语法
python
try:
# 可能出错的代码
num: int = int(input("请输入一个数字:"))
result: float = 10 / num
print(f"结果:{result}")
except ValueError:
# 处理 ValueError
print("输入无效,请输入数字!")
except ZeroDivisionError:
# 处理 ZeroDivisionError
print("不能除以零!")执行流程
┌─────────────────────────────────────────┐
│ try-except 执行流程 │
├─────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ try │ │
│ └────┬─────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ 正常 异常 │
│ │ │ │
│ │ ┌──────┴──────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 继续执行 ValueError ZeroDivision │
│ 处理 处理 │
│ │ │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ 继续执行 │
│ │
└─────────────────────────────────────────┘捕获多个异常
分别处理
python
def parse_and_divide(num_str: str, divisor_str: str) -> float | None:
"""解析字符串并除法运算"""
try:
num: int = int(num_str)
divisor: int = int(divisor_str)
result: float = num / divisor
return result
except ValueError:
print("❌ 输入的不是有效数字!")
return None
except ZeroDivisionError:
print("❌ 不能除以零!")
return None
# 使用
print(parse_and_divide("10", "2")) # 5.0
print(parse_and_divide("abc", "2")) # None,打印错误
print(parse_and_divide("10", "0")) # None,打印错误统一处理
python
def parse_and_divide_unified(num_str: str, divisor_str: str) -> float | None:
"""解析字符串并除法运算(统一错误处理)"""
try:
num: int = int(num_str)
divisor: int = int(divisor_str)
return num / divisor
except (ValueError, ZeroDivisionError) as e:
print(f"❌ 发生错误:{e}")
return Noneas 关键字的说明
as 用于将捕获的异常对象绑定到变量:
python
def divide_with_details(a: int, b: int) -> float | None:
"""除法运算,显示详细错误信息"""
try:
return a / b
except ZeroDivisionError as e:
# e 就是捕获到的异常对象
print(f"错误类型:{type(e).__name__}") # ZeroDivisionError
print(f"错误信息:{str(e)}") # division by zero
print(f"错误详情:{e.args}") # ('division by zero',)
return None注意: as 在 Python 中有多种用途,不限于异常处理:
┌─────────────────────────────────────────────────────┐
│ as 关键字的 3 种用途 │
├─────────────────────────────────────────────────────┤
│ │
│ 1. 异常处理 │
│ except ValueError as e: │
│ → 将异常对象绑定到变量 e │
│ │
│ 2. 模块导入别名 │
│ import numpy as np │
│ from math import sqrt as square_root │
│ → 给模块或函数起别名 │
│ │
│ 3. 上下文管理器 │
│ with open("file.txt") as f: │
│ → 将上下文管理器的返回值绑定到变量 │
│ │
└─────────────────────────────────────────────────────┘对比示例:
python
# 1. 异常处理中的 as
def example_exception() -> None:
try:
risky_operation()
except ValueError as e:
print(f"错误:{e}")
# 2. 模块导入中的 as
import numpy as np
from math import pi as PI
# 3. 上下文管理器中的 as
def example_context() -> None:
with open("file.txt") as f:
content: str = f.read()获取异常信息
python
def detailed_error_handling() -> None:
"""详细错误处理示例"""
try:
result: float = 10 / 0
except Exception as e:
# e 是异常对象
print(f"错误类型:{type(e).__name__}")
print(f"错误信息:{e}")
print(f"错误详情:{e.args}")
# 输出:
# 错误类型:ZeroDivisionError
# 错误信息:division by zero
# 错误详情:('division by zero',)else 和 finally 子句
else 子句
python
def divide_with_else(num_str: str, divisor_str: str) -> float | None:
"""使用 else 子句的除法运算"""
try:
num: int = int(num_str)
divisor: int = int(divisor_str)
result: float = num / divisor
except ValueError:
print("请输入有效的数字!")
return None
except ZeroDivisionError:
print("不能除以零!")
return None
else:
# 只有 try 块没有抛出异常时才执行
print(f"✅ 计算成功!结果:{result}")
return resultfinally 子句
python
def read_file_safely(filepath: str) -> str | None:
"""安全读取文件"""
file = None
try:
print("尝试打开文件...")
file = open(filepath, "r", encoding="utf-8")
content: str = file.read()
return content
except FileNotFoundError:
print("❌ 文件不存在")
return None
finally:
# 无论是否发生异常,都会执行
print("清理资源...")
if file is not None:
file.close()
print("文件已关闭")
# 更好的写法:使用 with 语句
def read_file_with_context(filepath: str) -> str | None:
"""使用上下文管理器读取文件"""
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print("❌ 文件不存在")
return Nonefinally 的应用场景
┌─────────────────────────────────────────┐
│ finally 的应用场景 │
├─────────────────────────────────────────┤
│ │
│ 📁 文件操作:关闭文件 │
│ 🔌 网络连接:关闭连接 │
│ 🔓 锁:释放锁 │
│ 🗄️ 数据库:关闭连接 │
│ │
│ finally 总是执行,确保资源释放 │
│ │
└─────────────────────────────────────────┘第五步:渐进复杂化
完整的异常处理结构
python
def divide_numbers() -> float | None:
"""完整的异常处理示例"""
try:
num1: int = int(input("请输入被除数:"))
num2: int = int(input("请输入除数:"))
result: float = num1 / num2
print(f"结果:{result}")
except ValueError:
print("❌ 错误:请输入有效的整数!")
except ZeroDivisionError:
print("❌ 错误:除数不能为零!")
except Exception as e:
print(f"❌ 发生未知错误:{type(e).__name__}: {e}")
else:
print("✅ 计算成功完成!")
return result
finally:
print("感谢使用,再见!")
return None异常链与原因追踪
python
def load_config(filepath: str) -> dict[str, str]:
"""
加载配置文件。
Args:
filepath: 配置文件路径
Returns:
配置字典
Raises:
FileNotFoundError: 配置文件不存在
ValueError: 配置格式错误
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
content: str = f.read()
except FileNotFoundError:
raise FileNotFoundError(f"配置文件不存在:{filepath}")
config: dict[str, str] = {}
for line_num, line in enumerate(content.split("\n"), 1):
if not line or line.startswith("#"):
continue
if "=" not in line:
raise ValueError(f"第 {line_num} 行格式错误:缺少 '='")
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config第六步:实际应用
用户输入验证
python
def get_valid_integer(
prompt: str,
min_value: int | None = None,
max_value: int | None = None
) -> int | None:
"""
获取用户输入的有效整数。
Args:
prompt: 提示信息
min_value: 最小值(可选)
max_value: 最大值(可选)
Returns:
有效的整数,如果用户取消返回 None
"""
while True:
try:
user_input: str = input(prompt)
if user_input.lower() in ("q", "quit", "exit"):
return None
value: int = int(user_input)
if min_value is not None and value < min_value:
print(f"❌ 输入值不能小于 {min_value}")
continue
if max_value is not None and value > max_value:
print(f"❌ 输入值不能大于 {max_value}")
continue
return value
except ValueError:
print("❌ 请输入有效的整数")
def main() -> None:
"""主程序:年龄输入示例"""
print("=== 用户信息录入 ===")
print("输入 'q' 退出\n")
age: int | None = get_valid_integer(
"请输入您的年龄(0-150):",
min_value=0,
max_value=150
)
if age is not None:
print(f"✅ 您的年龄是:{age} 岁")
else:
print("已取消")
if __name__ == "__main__":
main()文件处理安全模式
python
from pathlib import Path
from typing import Any
class FileHandler:
"""安全文件处理器"""
def __init__(self, filepath: str | Path):
self.filepath: Path = Path(filepath)
def read_text(self, encoding: str = "utf-8") -> str | None:
"""安全读取文本文件"""
try:
return self.filepath.read_text(encoding=encoding)
except FileNotFoundError:
print(f"❌ 文件不存在:{self.filepath}")
return None
except PermissionError:
print(f"❌ 无权限访问:{self.filepath}")
return None
except UnicodeDecodeError:
print(f"❌ 文件编码错误:{self.filepath}")
return None
def write_text(
self,
content: str,
encoding: str = "utf-8",
backup: bool = True
) -> bool:
"""安全写入文本文件"""
try:
# 备份原文件
if backup and self.filepath.exists():
backup_path: Path = self.filepath.with_suffix(
self.filepath.suffix + ".bak"
)
backup_path.write_text(
self.filepath.read_text(encoding=encoding),
encoding=encoding
)
self.filepath.write_text(content, encoding=encoding)
return True
except PermissionError:
print(f"❌ 无权限写入:{self.filepath}")
return False
except Exception as e:
print(f"❌ 写入失败:{type(e).__name__}: {e}")
return False
# 使用示例
def example_file_handler() -> None:
handler: FileHandler = FileHandler("config.txt")
# 读取
content: str | None = handler.read_text()
# 写入
if content is not None:
success: bool = handler.write_text(content + "\n新内容")
print(f"写入{'成功' if success else '失败'}")数据库操作安全模式
python
from typing import Generator, Any
class DatabaseConnection:
"""模拟数据库连接"""
def __init__(self, connection_string: str):
self.connection_string: str = connection_string
self.is_connected: bool = False
def connect(self) -> None:
"""建立连接"""
print(f"连接数据库:{self.connection_string}")
self.is_connected = True
def disconnect(self) -> None:
"""断开连接"""
print("断开数据库连接")
self.is_connected = False
def execute(self, query: str) -> list[dict[str, Any]]:
"""执行查询"""
if not self.is_connected:
raise RuntimeError("数据库未连接")
print(f"执行查询:{query}")
return [{"id": 1, "name": "Alice"}]
def with_database(
connection_string: str
) -> Generator[DatabaseConnection, None, None]:
"""数据库连接上下文管理器"""
db: DatabaseConnection = DatabaseConnection(connection_string)
try:
db.connect()
yield db
except Exception as e:
print(f"❌ 数据库错误:{e}")
raise
finally:
db.disconnect()
# 使用示例
def get_users() -> list[dict[str, Any]] | None:
"""获取所有用户"""
try:
with with_database("postgresql://localhost/mydb") as db:
return db.execute("SELECT * FROM users")
except Exception as e:
print(f"❌ 查询失败:{e}")
return None关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
except FileNotFoundError / PermissionError / UnicodeDecodeError | 分别捕获不同类型异常 | 每种错误原因不同,分开处理给出精准提示,比统一 except Exception 对用户更有帮助 |
backup_path.write_text(...); self.filepath.write_text(...) | 先写备份再写原文件 | 备份先行:若写原文件失败,备份已存在,数据不丢失 |
def with_database(...) -> Generator[...] | 用生成器函数实现上下文管理器 | yield 前是进入逻辑,yield 后(finally 中)是退出逻辑,比实现 __enter__/__exit__ 更简洁 |
except Exception as e: print(...); raise | 捕获后重新抛出 | 记录错误日志但不吞掉异常,让调用方仍能感知到错误 |
finally: db.disconnect() | 在 finally 中断开连接 | 无论成功还是异常,连接必须释放,finally 保证必定执行 |
L2 实践层:最佳实践
try-except 最佳实践
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 捕获具体异常类型 | 避意外捕获,精准处理 | except ValueError: |
| 从具体到宽泛排序 | 确保具体异常被正确处理 | ValueError → Exception |
使用 as 绑定异常对象 | 获取详细错误信息 | except ValueError as e: |
else 块放成功逻辑 | 分离正常流程和异常处理 | else: return result |
finally 只做资源清理 | 确保释放,不返回值 | finally: file.close() |
| 异常信息要有意义 | 帮助定位问题 | f"处理文件 {path} 失败" |
异常捕获顺序原则
┌─────────────────────────────────────────────────────────────────┐
│ 异常捕获顺序原则 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 原则:从具体到宽泛,从不重复 │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 正确顺序: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ try: │ │
│ │ operation() │ │
│ │ except ValueError: # 最具体 │ │
│ │ handle_value_error() │ │
│ │ except TypeError: # 另一个具体 │ │
│ │ handle_type_error() │ │
│ │ except Exception: # 兜底(可选) │ │
│ │ handle_general_error() │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 错误顺序: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ try: │ │
│ │ operation() │ │
│ │ except Exception: # ❌ 先捕获宽泛 │ │
│ │ handle_general_error() │ │
│ │ except ValueError: # ❌ 永远不会执行 │ │
│ │ handle_value_error() # 因为 ValueError 是 │ │
│ │ # Exception 的子类 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘else 和 finally 的最佳用法
python
# ✅ 推荐:else 放成功后的逻辑
def process_file(filepath: str) -> str | None:
"""处理文件的最佳模式"""
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
except FileNotFoundError:
logging.warning(f"文件不存在:{filepath}")
return None
except PermissionError:
logging.error(f"无权限访问:{filepath}")
return None
else:
# try 成功后才执行的业务逻辑
processed = content.strip().upper()
logging.info(f"处理成功:{filepath}")
return processed
finally:
# 清理工作(这里不需要额外清理,with 已处理)
logging.debug("文件操作完成")
# ❌ 不推荐:成功逻辑混在 try 里
def process_file_bad(filepath: str) -> str | None:
"""不推荐的写法"""
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
processed = content.strip().upper() # 混在 try 里
logging.info(f"处理成功:{filepath}")
return processed
except FileNotFoundError:
logging.warning(f"文件不存在:{filepath}")
return None
# 问题:processed 的错误也会被捕获,范围太宽关键区别:
| 写法 | try 块范围 | 问题 |
|---|---|---|
| 成功逻辑放 try 内 | 包含更多代码 | 后续逻辑错误也会被意外捕获 |
| 成功逻辑放 else | try 只做可能失败的操作 | 异常范围明确,后续错误不被掩盖 |
异常处理反模式
反模式 1:裸 except
python
# ❌ 最危险的反模式:裸 except
try:
result = risky_operation()
except: # 捕获一切,包括 KeyboardInterrupt、SystemExit
print("出错了")
# 问题:
# 1. KeyboardInterrupt 也被捕获,无法用 Ctrl+C 停止程序
# 2. SystemExit 被捕获,sys.exit() 不生效
# 3. 所有错误信息丢失,调试极困难python
# ✅ 正确做法:至少捕获 Exception
try:
result = risky_operation()
except Exception as e: # 只捕获 Exception 子类
print(f"出错了:{e}")
# KeyboardInterrupt 和 SystemExit 继承 BaseException,不会被捕获反模式 2:异常信息丢失
python
# ❌ 异常信息丢失
try:
result = int(user_input)
except ValueError:
raise ValueError("输入错误") # 原始信息丢失
# 问题:调用者只知道"输入错误",不知道具体是什么值python
# ✅ 保留原始信息
try:
result = int(user_input)
except ValueError as e:
raise ValueError(f"输入错误:{user_input}") from e
# 调用者可以看到原始错误和新增的上下文信息反模式 3:过度捕获
python
# ❌ 过度捕获:一个 try 包太多代码
def process_data_bad(data: dict) -> dict:
"""过度捕获的反模式"""
try:
# 100 行代码都在 try 里
result = validate(data)
result = transform(result)
result = save(result)
result = notify(result)
return result
except Exception:
print("处理失败") # 哪一步失败?不知道
return {}
# 问题:任何一步失败都只打印"处理失败",无法定位python
# ✅ 分段捕获:明确每一步的错误
def process_data_good(data: dict) -> dict | None:
"""分段处理,精准捕获"""
try:
result = validate(data)
except ValidationError as e:
logging.error(f"验证失败:{e}")
return None
try:
result = transform(result)
except TransformError as e:
logging.error(f"转换失败:{e}")
return None
try:
save(result)
except DatabaseError as e:
logging.error(f"保存失败:{e}")
return None
return result反模式汇总表
| 反模式 | 错误代码 | 问题 | 正确做法 |
|---|---|---|---|
裸 except: | except: | 捕获系统异常,无法中断程序 | except Exception as e: |
| 信息丢失 | raise ValueError("错误") | 原始错误详情丢失 | raise ... from e |
| 过度捕获 | 一个 try 包 100 行 | 无法定位具体失败位置 | 分段 try-except |
| 空处理 | except: pass | 错误完全被吞掉 | 至少记录日志 |
| finally 返回 | finally: return x | 覆盖 try 的返回值 | finally 只做清理 |
| 异常做流程 | try: x in list except: ... | 性能差,语义错误 | 先检查 if x in list |
适用场景
异常处理使用场景指南
| 场景 | 推荐程度 | 推荐做法 | 原因 |
|---|---|---|---|
| 文件操作 | ✅ 必须 | with open() + 具体异常 | 文件可能不存在、权限问题 |
| 网络/API | ✅ 必须 | 分段捕获 + 重试逻辑 | 网络不稳定,连接失败常见 |
| 数据解析 | ✅ 必须 | 具体异常 + 默认值 | 格式错误需要优雅降级 |
| 数据库操作 | ✅ 必须 | 事务 + 异常回滚 | 数据一致性要求 |
| 用户输入 | ✅ 必须 | 循环重试 + 提示 | 用户输入不可预测 |
| 内部逻辑检查 | ❌ 避免 | 用 assert 或条件判断 | 这些是开发时应避免的错误 |
| 元素查找 | ❌ 避免 | in 检查或 .get() | 不存在是常见情况,非异常 |
L3 专家层:底层原理
异常传播机制
Python 如何实现异常传播
┌─────────────────────────────────────────────────────────────────┐
│ 异常传播机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 异常传播路径: │
│ │
│ 函数调用栈: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ main() ← 捕获点 │ │
│ │ └── process_data() ← 可以捕获 │ │
│ │ └── parse_json() ← 可以捕获 │ │
│ │ └── json.loads() ← 抛出点 │ │
│ │ raise JSONDecodeError │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 传播过程: │
│ 1. json.loads() 抛出 JSONDecodeError │
│ 2. 异常沿调用栈向上传播 │
│ 3. 遇到 except 块 → 被捕获处理 │
│ 4. 无 except 块 → 继续向上 │
│ 5. 到达顶层 → 程序终止,打印 traceback │
│ │
└─────────────────────────────────────────────────────────────────┘异常对象的结构
python
# 演示异常对象的内部结构
def demonstrate_exception_structure() -> None:
"""展示异常对象包含的信息"""
try:
result = 10 / 0
except ZeroDivisionError as e:
# 异常对象的属性
print(f"类型:{type(e)}") # ZeroDivisionError
print(f"消息:{str(e)}") # division by zero
print(f"参数:{e.args}") # ('division by zero',)
# traceback 信息
import traceback
print(f"追踪:\n{traceback.format_exc()}")
# 输出:
# 类型:<class 'ZeroDivisionError'>
# 消息:division by zero
# 参数:('division by zero',)
# 追踪:
# Traceback (most recent call last):
# File "...", line X, in demonstrate_exception_structure
# result = 10 / 0
# ZeroDivisionError: division by zerotraceback 的作用
┌─────────────────────────────────────────────────────────────────┐
│ Traceback 结构解析 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Traceback (most recent call last): │
│ File "app.py", line 50, in main │
│ process_user(user_id) │
│ File "app.py", line 35, in process_user │
│ data = fetch_from_db(user_id) │
│ File "app.py", line 20, in fetch_from_db │
│ result = db.query(sql) │
│ ConnectionError: Database connection failed │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 信息解读: │
│ • 从上到下是调用链的逆序(最近的调用在最上面) │
│ • 每条记录包含:文件名、行号、函数名、执行代码 │
│ • 最后一行是异常类型和消息 │
│ │
│ 使用技巧: │
│ • 从最后一行开始看(知道是什么错误) │
│ • 从上往下追踪(找到错误发生的位置) │
│ • 关注自己写的代码行(忽略库内部调用) │
│ │
└─────────────────────────────────────────────────────────────────┘异常链机制
Python 如何实现异常链
┌─────────────────────────────────────────────────────────────────┐
│ 异常链机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 异常链:保留原始异常,添加新的上下文 │
│ │
│ 两种方式: │
│ │
│ 1. raise ... from e(显式链) │
│ ───────────────────────────────────────────────────────── │
│ try: │
│ low_level_operation() │
│ except LowLevelError as e: │
│ raise HighLevelError("上下文信息") from e │
│ │
│ 结果: │
│ HighLevelError: 上下文信息 │
│ The above exception was the direct cause of... │
│ LowLevelError: 原始错误详情 │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 2. raise 新异常(隐式链) │
│ ───────────────────────────────────────────────────────── │
│ try: │
│ operation() │
│ except OriginalError: │
│ raise NewError("新信息") # 不用 from │
│ │
│ 结果: │
│ NewError: 新信息 │
│ During handling of above exception, another exception occurred │
│ OriginalError: 原始错误 │
│ │
│ 注:隐式链用 __context__,显式链用 __cause__ │
│ │
└─────────────────────────────────────────────────────────────────┘异常链的属性
python
# 演示 __cause__ 和 __context__ 的区别
def demonstrate_exception_chain() -> None:
"""展示异常链的内部属性"""
# 显式链(from)
try:
try:
raise ValueError("原始错误")
except ValueError as e:
raise RuntimeError("包装错误") from e
except RuntimeError as e:
print(f"__cause__: {e.__cause__}") # ValueError("原始错误")
print(f"__context__: {e.__context__}") # ValueError("原始错误")
# __cause__ 表示"这是原因",显式设置
print("---")
# 隐式链(不用 from)
try:
try:
raise ValueError("原始错误")
except ValueError:
raise RuntimeError("新错误") # 处理原异常时抛出新异常
except RuntimeError as e:
print(f"__cause__: {e.__cause__}") # None
print(f"__context__: {e.__context__}") # ValueError("原始错误")
# __context__ 表示"处理这个时发生了另一个"
# 输出:
# __cause__: 原始错误
# __context__: 原始错误
# ---
# __cause__: None
# __context__: 原始错误断开异常链
python
# 有时需要断开异常链(不显示原始错误)
try:
raise ValueError("不想暴露的错误")
except ValueError:
raise RuntimeError("新的错误") from None # from None 断开链
# 结果:只显示 RuntimeError,不显示 ValueError性能影响
异常处理的性能特性
┌─────────────────────────────────────────────────────────────────┐
│ 异常处理性能分析 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 关键结论: │
│ │
│ 1. try 块本身几乎无开销 │
│ • 不发生异常时:try 设置成本极低 │
│ • 现代 Python 有专门优化 │
│ │
│ 2. 异常抛出/捕获有显著开销 │
│ • 需要构建 traceback 对象 │
│ • 需要遍历调用栈 │
│ • 比条件检查慢 10-100 倍 │
│ │
│ 3. 异常应该用于"异常情况" │
│ • 预期会频繁发生的情况 → 用条件检查 │
│ • 偶尔发生的情况 → 用异常 │
│ │
└─────────────────────────────────────────────────────────────────┘性能测试对比
python
import timeit
# 测试 1:异常 vs 条件检查(元素查找)
def test_exception() -> bool:
"""用异常处理不存在"""
try:
_ = my_dict["missing_key"]
return True
except KeyError:
return False
def test_condition() -> bool:
"""用条件检查不存在"""
if "missing_key" in my_dict:
_ = my_dict["missing_key"]
return True
return False
my_dict = {"a": 1, "b": 2}
# 条件检查更快(不存在是常见情况)
time_exception = timeit.timeit(test_exception, number=100000)
time_condition = timeit.timeit(test_condition, number=100000)
print(f"异常方式:{time_exception:.4f}s")
print(f"条件检查:{time_condition:.4f}s")
print(f"条件检查快 {time_exception/time_condition:.1f} 倍")
# 输出(典型):
# 异常方式:0.15s
# 条件检查:0.02s
# 条件检查快 7 倍性能对比表
| 操作 | 无异常发生 | 异常发生 | 建议 |
|---|---|---|---|
try 设置 | ~0 开销 | - | 放心使用 |
条件检查 if | 低开销 | 低开销 | 频繁失败时使用 |
| 异常抛出/捕获 | - | 高开销 | 偶尔失败时使用 |
| 构建 traceback | - | 很高开销 | 异常已发生,不可避免 |
性能优化建议
| 场景 | 预期失败频率 | 推荐方法 | 原因 |
|---|---|---|---|
| 字典键查找 | 可能频繁不存在 | .get() 或 in | 避免 traceback 开销 |
| 列表元素查找 | 可能频繁不存在 | in 检查 | 同上 |
| 文件不存在 | 偶尔发生 | 异常处理 | 文件操作本身就慢 |
| 网络连接失败 | 常见 | 异常处理 | 网络延迟更大,异常开销可忽略 |
| 数据格式错误 | 偶尔发生 | 异常处理 | 解析本身就耗时 |
设计动机
Python 为什么这样设计异常
┌─────────────────────────────────────────────────────────────────┐
│ 异常设计的动机 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 设计选择 原因 │
│ ───────────────────────────────────────────────────────── │
│ │
│ 异常是对象(Exception 类) │
│ • 可以携带丰富的错误信息 │
│ • 可以自定义异常类型 │
│ • 可以检查异常属性 │
│ • 对比:C 用整数错误码,信息有限 │
│ │
│ 异常沿调用栈自动传播 │
│ • 调用者不必每步检查返回值 │
│ • 错误不会被忽略(不捕获就终止) │
│ • 对比:Go 需要每步 if err != nil │
│ │
│ try-except-finally 结构 │
│ • 业务逻辑和错误处理分离 │
│ • finally 确保资源释放 │
│ • 对比:C 需要手动检查和清理 │
│ │
│ 异常链(raise ... from) │
│ • 保留原始错误上下文 │
│ • 便于调试和追踪 │
│ • Python 3 特有,解决"信息丢失"问题 │
│ │
└─────────────────────────────────────────────────────────────────┘与其他语言的对比
| 特性 | Python | Java | Go | C |
|---|---|---|---|---|
| 异常类型 | 对象,可继承 | 对象,checked/unchecked | 无异常,返回 error | 整数错误码 |
| 错误传播 | 自动传播 | 自动传播 | 手动检查 if err != nil | 手动检查 |
| 强制处理 | 不强制(不捕获终止) | checked 必须处理 | 不强制 | 不强制 |
| 资源清理 | finally / with | finally | defer | 手动清理 |
| 异常链 | from 关键字 | cause 属性 | 无 | 无 |
知识关联
┌─────────────────────────────────────────────────────────────────┐
│ 异常处理知识关联图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ │
│ │ BaseException │ │
│ │ (基类) │ │
│ └───────────────────┘ │
│ │ │
│ ↓ │
│ ┌───────────────────┐ │
│ │ Exception │ │
│ │ (可捕获异常) │ │
│ └───────────────────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ │ │ │ │
│ ↓ ↓ ↓ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ ValueError│ │TypeError │ │自定义异常 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ↓ ↓ ↓ │
│ ┌───────────────────────────────────────┐ │
│ │ try-except │ │
│ │ (捕获和处理) │ │
│ └───────────────────────────────────────┘ │
│ │ │ │
│ ↓ ↓ │
│ ┌───────────┐ ┌───────────┐ │
│ │ raise │ │ with │ │
│ │ (主动抛出)│ │(资源管理) │ │
│ └───────────┘ └───────────┘ │
│ │ │ │
│ ↓ ↓ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 异常链 │ │上下文管理 │ │
│ │from 语法 │ │__enter__ │ │
│ │ │ │__exit__ │ │
│ └───────────┘ └───────────┘ │
│ │
│ 前置知识:类与继承、函数调用栈 │
│ 进阶扩展:上下文管理器、自定义异常设计 │
│ │
└─────────────────────────────────────────────────────────────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 异常处理 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ try-except: │
│ ✓ try:可能出错的代码 │
│ ✓ except:捕获并处理异常 │
│ │
│ 捕获方式: │
│ ✓ except ValueError: 单独处理 │
│ ✓ except (A, B) as e: 统一处理 │
│ ✓ except Exception: 捕获所有 │
│ │
│ else: │
│ ✓ 没有异常时执行 │
│ │
│ finally: │
│ ✓ 总是执行,用于清理资源 │
│ │
└─────────────────────────────────────────────────────────────┘