03-抛出异常
Python 版本要求: 本教程基于 Python 3.11+ 编写,部分特性需要 3.11 或更高版本。
导航: 错误与异常基础 → 异常处理 → 抛出异常 → 上下文管理器
概念铺垫
第一步:实际场景引入
为什么需要主动抛出异常?
假设你正在开发一个银行系统:
python
def withdraw(balance: float, amount: float) -> float:
"""取款"""
return balance - amount
# 问题来了:
result = withdraw(100, 200) # 返回 -100,余额为负?
result = withdraw(100, -50) # 返回 150,负数取款?
result = withdraw(100, "abc") # 类型错误?问题来了:
场景 1: 取款金额超过余额
→ 返回负数余额?不合理!
→ 需要阻止操作并提示
场景 2: 取款金额为负数
→ 无效操作
→ 需要拒绝并告知原因
场景 3: 参数类型错误
→ 应该提前发现并处理思考: 如何在函数内部主动报告错误,并阻止无效操作?
第二步:概念动机
主动抛出异常的意义
┌─────────────────────────────────────────┐
│ 主动抛出异常的意义 │
├─────────────────────────────────────────┤
│ │
│ 核心思想: │
│ ───────────────────────── │
│ 当函数无法完成其职责时, │
│ 主动抛出异常通知调用者。 │
│ │
│ ───────────────────────────────── │
│ │
│ 为什么不返回 None 或错误码? │
│ │
│ 返回 None/错误码: │
│ • 调用者容易忘记检查 │
│ • 无法携带详细错误信息 │
│ • 错误会在代码中悄悄传播 │
│ │
│ 抛出异常: │
│ • 强制调用者处理 │
│ • 可以携带丰富的错误信息 │
│ • 错误不会被忽略 │
│ │
│ ───────────────────────────────── │
│ │
│ 什么时候抛出异常? │
│ • 参数无效 │
│ • 前置条件不满足 │
│ • 资源不可用 │
│ • 业务规则违反 │
│ │
└─────────────────────────────────────────┘核心理解:
- raise:主动抛出异常
- 自定义异常:创建有意义的异常类型
- 异常链:保留原始异常信息
L1 理解层:会用
第三步:最简示例
使用 raise 抛出异常
python
def set_age(age: int) -> None:
"""设置年龄"""
if age < 0 or age > 150:
raise ValueError("年龄必须在 0-150 之间")
print(f"年龄设置为:{age}")
# 使用
set_age(25) # ✅ 正常
# set_age(-5) # ❌ 抛出 ValueError
# set_age(200) # ❌ 抛出 ValueError第四步:详细讲解
raise 语句
基本用法
python
def set_age(age: int) -> None:
"""设置年龄"""
if age < 0 or age > 150:
raise ValueError("年龄必须在 0-150 之间")
print(f"年龄设置为:{age}")
# 使用
set_age(25) # ✅ 正常
# set_age(-5) # ❌ 抛出 ValueError
# set_age(200) # ❌ 抛出 ValueError抛出带详细信息的异常
python
def withdraw(balance: float, amount: float) -> float:
"""
取款操作。
Args:
balance: 当前余额
amount: 取款金额
Returns:
取款后的余额
Raises:
ValueError: 取款金额无效
"""
if amount <= 0:
raise ValueError(f"取款金额必须大于 0,当前输入:{amount}")
if amount > balance:
raise ValueError(f"余额不足!余额:{balance},尝试取款:{amount}")
return balance - amount
# 使用
try:
new_balance: float = withdraw(100, 200)
except ValueError as e:
print(f"❌ 取款失败:{e}")重新抛出异常
使用 raise
python
def process_data(data: str) -> int:
"""
处理数据。
Args:
data: 输入数据字符串
Returns:
转换后的整数
Raises:
ValueError: 数据格式错误
"""
try:
result: int = int(data)
return result
except ValueError as e:
print(f"捕获到错误:{e}")
print("尝试记录日志...")
raise # 重新抛出当前异常
# 使用
try:
process_data("abc")
except ValueError:
print("最终捕获到异常")捕获一个异常,抛出另一个
python
def divide(a: float, b: float) -> float:
"""
安全除法。
Args:
a: 被除数
b: 除数
Returns:
商
Raises:
ValueError: 除数为零
"""
try:
result: float = a / b
return result
except ZeroDivisionError as e:
# 包装成更有意义的异常
raise ValueError(f"除数不能为零:a={a}, b={b}") from e
# 使用
try:
divide(10, 0)
except ValueError as e:
print(f"捕获到:{e}")异常链追踪
python
def load_user_config(filepath: str) -> dict[str, str]:
"""加载用户配置"""
try:
with open(filepath, "r", encoding="utf-8") as f:
return parse_config(f.read())
except FileNotFoundError as e:
raise RuntimeError(f"配置文件不存在:{filepath}") from e
except ValueError as e:
raise RuntimeError("配置文件格式错误") from e
def parse_config(content: str) -> dict[str, str]:
"""解析配置内容"""
config: dict[str, str] = {}
for line in content.split("\n"):
if "=" in line:
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config
# 使用时可以追踪异常链
try:
config = load_user_config("missing.txt")
except RuntimeError as e:
print(f"错误:{e}")
if e.__cause__:
print(f"原始错误:{e.__cause__}")自定义异常类
基础自定义异常
python
class MyError(Exception):
"""自定义异常基类"""
pass
# 使用
try:
raise MyError("这是一个自定义错误")
except MyError as e:
print(f"捕获到自定义错误:{e}")带属性的自定义异常
python
class ValidationError(Exception):
"""验证异常"""
def __init__(self, field: str, message: str) -> None:
self.field: str = field
self.message: str = message
super().__init__(f"{field}: {message}")
def __str__(self) -> str:
return f"字段 '{self.field}' 验证失败:{self.message}"
# 使用
try:
raise ValidationError("email", "邮箱格式不正确")
except ValidationError as e:
print(e) # 字段 'email' 验证失败:邮箱格式不正确
print(f"字段:{e.field}") # email
print(f"消息:{e.message}") # 邮箱格式不正确异常层次结构
python
# 应用错误基类
class AppError(Exception):
"""应用错误基类"""
pass
# 数据库相关错误
class DatabaseError(AppError):
"""数据库错误"""
pass
class ConnectionError(DatabaseError):
"""数据库连接错误"""
pass
# 用户相关错误
class UserError(AppError):
"""用户相关错误"""
pass
class AuthenticationError(UserError):
"""认证错误"""
pass
class PermissionError(UserError):
"""权限错误"""
pass
# 使用
def login(username: str, password: str) -> dict[str, str]:
"""用户登录"""
if not username:
raise AuthenticationError("用户名不能为空")
if password != "correct_password":
raise AuthenticationError("用户名或密码错误")
return {"username": username, "status": "logged_in"}
try:
login("", "123456")
except AppError as e:
print(f"应用错误:{e}")断言(assert)
基本用法
python
def divide(a: float, b: float) -> float:
"""除法运算(使用断言)"""
assert b != 0, "除数不能为零"
return a / b
# 使用
result1: float = divide(10, 2) # ✅ 5
# result2: float = divide(10, 0) # ❌ AssertionError: 除数不能为零断言 vs 异常
┌─────────────────────────────────────────┐
│ 断言与异常的区别 │
├─────────────────────────────────────────┤
│ │
│ 断言 (assert) │
│ ────────────── │
│ 用途:调试和测试 │
│ 生产环境:可能禁用(python -O) │
│ 错误类型:AssertionError │
│ │
│ 异常 (raise) │
│ ────────────── │
│ 用途:处理运行时错误 │
│ 生产环境:总是生效 │
│ 错误类型:任意 Exception 子类 │
│ │
│ ───────────────────────────────── │
│ │
│ 选择指南: │
│ • 用户输入验证 → 用异常 │
│ • 程序逻辑检查 → 用断言 │
│ │
└─────────────────────────────────────────┘代码示例:
python
def calculate_average(numbers: list[float]) -> float:
"""计算平均值"""
# 断言:检查内部逻辑(开发时发现问题)
assert len(numbers) > 0, "列表不能为空(这是程序员的错误)"
return sum(numbers) / len(numbers)
def get_average_from_user() -> float | None:
"""从用户输入计算平均值"""
user_input: str = input("请输入数字列表(逗号分隔):")
# 异常:处理用户输入(运行时可能发生)
try:
numbers: list[float] = [float(x.strip()) for x in user_input.split(",")]
if len(numbers) == 0:
print("❌ 请至少输入一个数字")
return None
return calculate_average(numbers)
except ValueError:
print("❌ 输入格式错误")
return None第五步:渐进复杂化
构建异常体系
python
from typing import Optional
# 基础异常
class AppException(Exception):
"""应用基础异常"""
def __init__(self, message: str, code: Optional[int] = None) -> None:
self.message: str = message
self.code: Optional[int] = code
super().__init__(message)
def __str__(self) -> str:
if self.code:
return f"[{self.code}] {self.message}"
return self.message
# 业务异常
class BusinessException(AppException):
"""业务异常基类"""
pass
class ValidationException(BusinessException):
"""验证异常"""
def __init__(
self,
field: str,
message: str,
code: Optional[int] = None
) -> None:
self.field: str = field
super().__init__(f"{field}: {message}", code)
class NotFoundException(BusinessException):
"""资源未找到异常"""
def __init__(
self,
resource: str,
identifier: str,
code: Optional[int] = None
) -> None:
self.resource: str = resource
self.identifier: str = identifier
super().__init__(f"{resource} 未找到:{identifier}", code)
# 使用示例
def get_user(user_id: int) -> dict[str, str | int]:
"""获取用户"""
users: dict[int, dict[str, str | int]] = {
1: {"id": 1, "name": "Alice"},
2: {"id": 2, "name": "Bob"},
}
if user_id <= 0:
raise ValidationException(
"user_id",
"用户ID必须为正整数",
code=400
)
if user_id not in users:
raise NotFoundException(
"用户",
str(user_id),
code=404
)
return users[user_id]
# 异常处理
def handle_request(user_id_str: str) -> dict[str, str | int] | None:
"""处理请求"""
try:
user_id: int = int(user_id_str)
return get_user(user_id)
except ValidationException as e:
print(f"验证错误 {e.code}: {e}")
return None
except NotFoundException as e:
print(f"资源未找到 {e.code}: {e}")
return None
except BusinessException as e:
print(f"业务错误 {e.code}: {e}")
return None第六步:实际应用
用户注册验证
python
import re
from typing import Optional
from dataclasses import dataclass
@dataclass
class User:
"""用户数据类"""
username: str
email: str
age: int
password: str
class RegistrationError(Exception):
"""注册错误"""
def __init__(self, field: str, message: str) -> None:
self.field: str = field
self.message: str = message
super().__init__(f"{field}: {message}")
def validate_username(username: str) -> None:
"""验证用户名"""
if not username:
raise RegistrationError("username", "用户名不能为空")
if len(username) < 3:
raise RegistrationError("username", "用户名至少 3 个字符")
if len(username) > 20:
raise RegistrationError("username", "用户名最多 20 个字符")
if not re.match(r"^[a-zA-Z0-9_]+$", username):
raise RegistrationError("username", "用户名只能包含字母、数字和下划线")
def validate_email(email: str) -> None:
"""验证邮箱"""
if not email:
raise RegistrationError("email", "邮箱不能为空")
pattern: str = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, email):
raise RegistrationError("email", "邮箱格式不正确")
def validate_age(age: int) -> None:
"""验证年龄"""
if age < 0:
raise RegistrationError("age", "年龄不能为负数")
if age > 150:
raise RegistrationError("age", "年龄不能超过 150")
def validate_password(password: str) -> None:
"""验证密码"""
if not password:
raise RegistrationError("password", "密码不能为空")
if len(password) < 8:
raise RegistrationError("password", "密码至少 8 个字符")
if not re.search(r"[A-Z]", password):
raise RegistrationError("password", "密码必须包含大写字母")
if not re.search(r"[a-z]", password):
raise RegistrationError("password", "密码必须包含小写字母")
if not re.search(r"[0-9]", password):
raise RegistrationError("password", "密码必须包含数字")
def register_user(
username: str,
email: str,
age: int,
password: str
) -> User:
"""
注册用户。
Args:
username: 用户名
email: 邮箱
age: 年龄
password: 密码
Returns:
注册成功的用户对象
Raises:
RegistrationError: 注册验证失败
"""
validate_username(username)
validate_email(email)
validate_age(age)
validate_password(password)
# 模拟保存到数据库
user: User = User(
username=username,
email=email,
age=age,
password=password
)
print(f"✅ 用户注册成功:{username}")
return user
# 使用示例
def main() -> None:
"""主程序"""
print("=== 用户注册 ===\n")
# 测试数据
test_cases: list[dict[str, str | int]] = [
{"username": "alice", "email": "alice@example.com", "age": 25, "password": "Password123"},
{"username": "ab", "email": "alice@example.com", "age": 25, "password": "Password123"}, # 用户名太短
{"username": "bob", "email": "invalid-email", "age": 25, "password": "Password123"}, # 邮箱格式错误
{"username": "charlie", "email": "charlie@example.com", "age": -5, "password": "Password123"}, # 年龄负数
{"username": "david", "email": "david@example.com", "age": 30, "password": "weak"}, # 密码太弱
]
for i, data in enumerate(test_cases, 1):
print(f"测试 {i}: {data['username']}")
try:
user: User = register_user(
str(data["username"]),
str(data["email"]),
int(data["age"]),
str(data["password"])
)
except RegistrationError as e:
print(f"❌ 注册失败 - {e.field}: {e.message}")
print()
if __name__ == "__main__":
main()API 错误处理
python
from typing import Optional, Any
from dataclasses import dataclass
from enum import Enum
class ErrorCode(Enum):
"""错误代码枚举"""
INVALID_INPUT = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
INTERNAL_ERROR = 500
@dataclass
class APIResponse:
"""API 响应"""
success: bool
data: Optional[Any] = None
error: Optional[str] = None
code: Optional[int] = None
class APIError(Exception):
"""API 错误"""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.INTERNAL_ERROR
) -> None:
self.message: str = message
self.code: ErrorCode = code
super().__init__(message)
def to_response(self) -> APIResponse:
"""转换为 API 响应"""
return APIResponse(
success=False,
error=self.message,
code=self.code.value
)
def get_user_by_id(user_id: int) -> dict[str, str | int]:
"""获取用户"""
users: dict[int, dict[str, str | int]] = {
1: {"id": 1, "name": "Alice", "email": "alice@example.com"},
2: {"id": 2, "name": "Bob", "email": "bob@example.com"},
}
if user_id <= 0:
raise APIError("无效的用户ID", ErrorCode.INVALID_INPUT)
if user_id not in users:
raise APIError("用户不存在", ErrorCode.NOT_FOUND)
return users[user_id]
def handle_api_request(user_id_str: str) -> APIResponse:
"""处理 API 请求"""
try:
user_id: int = int(user_id_str)
user: dict[str, str | int] = get_user_by_id(user_id)
return APIResponse(success=True, data=user)
except APIError as e:
return e.to_response()
except ValueError:
return APIResponse(
success=False,
error="用户ID必须是整数",
code=ErrorCode.INVALID_INPUT.value
)
except Exception as e:
return APIResponse(
success=False,
error=f"服务器内部错误:{e}",
code=ErrorCode.INTERNAL_ERROR.value
)
# 使用示例
if __name__ == "__main__":
print("=== API 错误处理示例 ===\n")
# 测试请求
requests: list[str] = ["1", "2", "0", "-1", "999", "abc"]
for req in requests:
print(f"请求:GET /users/{req}")
response: APIResponse = handle_api_request(req)
if response.success:
print(f"✅ 成功:{response.data}")
else:
print(f"❌ 错误 [{response.code}]: {response.error}")
print()关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
class ErrorCode(Enum) | 用枚举定义错误码 | 枚举防止错误码写错(如拼写错误),代码中使用 ErrorCode.NOT_FOUND 比裸字符串更安全 |
class APIError(Exception) | 自定义异常类 | 携带 code 属性,让捕获方获取结构化错误信息,而不仅是消息字符串 |
def to_response(self) -> APIResponse | 异常转为响应对象的方法 | 将异常到响应的映射逻辑封装在异常类内,except APIError as e: e.to_response() 更简洁 |
except APIError as e: return e.to_response() | 捕获自定义异常并转换 | 精确捕获业务异常,让代码意图清晰;ValueError/Exception 兜底处理其他情况 |
raise APIError("用户不存在", ErrorCode.NOT_FOUND) | 主动抛出带错误码的异常 | 与其返回 None 让调用方猜测失败原因,不如抛出携带错误码的异常,信息更完整 |
L2 实践层:最佳实践
自定义异常设计
何时需要自定义异常
┌─────────────────────────────────────────────────────────────────┐
│ 自定义异常决策指南 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 需要自定义异常的场景: │
│ │
│ 1. 业务规则违反 │
│ ───────────────────────────────────────────────────────── │
│ 例:余额不足、库存不够、权限级别不够 │
│ 标准异常无法表达业务语义 │
│ │
│ 2. 需要携带额外信息 │
│ ───────────────────────────────────────────────────────── │
│ 例:验证失败需要记录哪个字段、什么问题 │
│ 标准异常只有消息字符串 │
│ │
│ 3. 需要统一处理一类错误 │
│ ───────────────────────────────────────────────────────── │
│ 例:所有 API 错误统一捕获为 APIError │
│ 便于在顶层统一处理 │
│ │
│ 4. 需要与其他错误区分 │
│ ───────────────────────────────────────────────────────── │
│ 例:配置错误不应被普通 Exception 捕获 │
│ 应有单独的异常类型 │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 不需要自定义异常的场景: │
│ │
│ • 参数类型错误 → TypeError(标准异常足够) │
│ • 参数值无效 → ValueError(标准异常足够) │
│ • 简单的错误提示 → 直接用标准异常 │
│ • 内部调试错误 → assert(非异常) │
│ │
└─────────────────────────────────────────────────────────────────┘自定义异常设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 继承自 Exception | 不要继承 BaseException | class MyError(Exception) |
| 命名以 Error/Exception 结尾 | 符合 Python 惯例 | ValidationError、DatabaseError |
| 提供清晰的错误信息 | 消息要能帮助定位问题 | f"字段 '{field}' 不能为空" |
| 携带上下文属性 | 便于程序化处理 | self.field = field |
| 建立层次结构 | 便于统一处理一类错误 | DatabaseError → ConnectionError |
提供 __str__ 方法 | 打印时有友好格式 | return f"[{self.code}] {self.message}" |
自定义异常模板
python
# ✅ 推荐:完整的自定义异常模板
class AppError(Exception):
"""应用错误基类
所有业务异常都应继承此类,便于统一捕获处理。
"""
def __init__(self, message: str, code: int | None = None) -> None:
self.message = message
self.code = code
super().__init__(message)
def __str__(self) -> str:
if self.code:
return f"[{self.code}] {self.message}"
return self.message
def __repr__(self) -> str:
return f"{self.__class__.__name__}(message='{self.message}', code={self.code})"
class ValidationError(AppError):
"""验证错误
用于用户输入验证失败等场景。
"""
def __init__(
self,
field: str,
message: str,
value: Any | None = None,
code: int = 400
) -> None:
self.field = field
self.value = value
super().__init__(f"{field}: {message}", code)
def __str__(self) -> str:
if self.value is not None:
return f"[{self.code}] {self.field}: {self.message} (值: {self.value})"
return f"[{self.code}] {self.field}: {self.message}"
class NotFoundError(AppError):
"""资源未找到
用于查找资源失败的场景。
"""
def __init__(
self,
resource: str,
identifier: str | int,
code: int = 404
) -> None:
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} 未找到: {identifier}", code)
# 使用示例
def get_user(user_id: int) -> dict:
users = {1: {"name": "Alice"}}
if user_id not in users:
raise NotFoundError("用户", user_id)
return users[user_id]
# 统一处理
try:
user = get_user(999)
except AppError as e:
print(f"错误:{e}") # 自动包含 code 和 message
logging.error(f"AppError: {e.code} - {e.message}")raise 最佳实践
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 抛出具体异常类型 | 明确错误类型便于处理 | raise ValueError("年龄无效") |
| 异常消息包含上下文 | 帮助定位问题位置 | raise ValueError(f"第{line}行格式错误") |
用 raise ... from e 保留原始异常 | 完整追踪错误链 | raise ConfigError() from e |
| 在函数文档声明异常 | 调用者知道可能抛什么 | Raises: ValueError: ... |
| 不要用异常做控制流 | 性能差,语义错误 | 用 if 判断 |
raise 的正确用法
python
# ✅ 推荐:抛出具体异常,包含上下文
def process_line(line: str, line_num: int) -> dict:
"""解析配置行"""
if "=" not in line:
raise ValueError(f"第 {line_num} 行格式错误:缺少 '=' 分隔符")
key, value = line.split("=", 1)
if not key.strip():
raise ValueError(f"第 {line_num} 行格式错误:键为空")
return {key.strip(): value.strip()}
# ✅ 推荐:使用异常链保留原始错误
def load_config(filepath: str) -> dict:
"""加载配置文件"""
try:
with open(filepath) as f:
content = f.read()
except FileNotFoundError as e:
raise ConfigError(f"配置文件不存在:{filepath}") from e
except PermissionError as e:
raise ConfigError(f"无权限读取配置文件:{filepath}") from e
try:
return parse_config(content)
except ValueError as e:
raise ConfigError(f"配置文件格式错误") from e
# ❌ 不推荐:异常信息太模糊
def process_data(data: dict) -> dict:
if not data:
raise ValueError("错误") # 什么错误?不知道
# ...在函数文档中声明异常
python
# ✅ 推荐:在 docstring 中声明可能抛出的异常
def divide(a: float, b: float) -> float:
"""
安全除法。
Args:
a: 被除数
b: 除数
Returns:
商
Raises:
ValueError: 除数为零时抛出
TypeError: 参数不是数字时抛出
"""
if b == 0:
raise ValueError("除数不能为零")
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("参数必须是数字")
return a / b
# 调用者知道需要捕获什么
try:
result = divide(10, 0)
except ValueError as e:
print(f"计算错误:{e}")
except TypeError as e:
print(f"类型错误:{e}")断言 vs 异常的选择
断言与异常的适用场景
┌─────────────────────────────────────────────────────────────────┐
│ 断言 vs 异常 选择指南 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 断言(assert): │
│ ───────────────────────────────────────────────────────── │
│ • 用于检查**程序内部逻辑** │
│ • 发现**程序员犯的错误** │
│ • 在开发/测试阶段发现问题 │
│ • 运行时可能被禁用(python -O) │
│ │
│ 例: │
│ assert result > 0, "计算结果必须为正数" │
│ assert len(items) > 0, "列表不应该为空(这是 bug)" │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 异常(raise): │
│ ───────────────────────────────────────────────────────── │
│ • 用于处理**外部因素导致的错误** │
│ • 发现**用户/环境的错误** │
│ • 在生产环境也需要处理 │
│ • 总是生效,不会被禁用 │
│ │
│ 例: │
│ if age < 0: │
│ raise ValueError("年龄不能为负数") │
│ if not file.exists(): │
│ raise FileNotFoundError(f"文件不存在:{path}") │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ 选择原则: │
│ 如果是"我不应该犯的错误" → assert │
│ 如果是"用户/环境可能造成的问题" → raise │
│ │
└─────────────────────────────────────────────────────────────────┘对比示例
python
# ✅ 正确使用 assert:检查内部逻辑
def calculate_average(numbers: list[float]) -> float:
"""计算平均值"""
# 这是程序员的错误,应该用断言
assert len(numbers) > 0, "列表不能为空(这是 bug,不是用户错误)"
assert all(isinstance(x, (int, float)) for x in numbers), \
"列表元素必须是数字(这是 bug)"
return sum(numbers) / len(numbers)
# ✅ 正确使用 raise:处理外部因素
def get_age_from_user() -> int:
"""获取用户年龄"""
user_input = input("请输入年龄:")
try:
age = int(user_input)
except ValueError:
raise ValueError("请输入有效的数字")
# 这是用户的错误,应该用异常
if age < 0:
raise ValueError("年龄不能为负数")
if age > 150:
raise ValueError("年龄不合理(超过 150)")
return age
# ❌ 错误用法:用 assert 检查用户输入
def get_age_bad() -> int:
"""错误示例"""
age = int(input("年龄:"))
assert age >= 0, "年龄不能为负数" # ❌ 用户输入不应用 assert
# 问题:python -O 运行时,assert 被禁用,检查失效
return age
# ❌ 错误用法:用异常检查内部逻辑
def internal_function_bad(data: list) -> int:
"""错误示例"""
# 这是程序员的错误,不应该让调用者捕获
if len(data) == 0:
raise ValueError("列表为空") # ❌ 应该用 assert
return data[0]适用场景
raise 使用场景指南
| 场景 | 推荐程度 | 推荐异常 | 原因 |
|---|---|---|---|
| 用户输入验证失败 | ✅ 必须 | ValueError 或自定义 | 用户可能输入任何值 |
| 资源不存在 | ✅ 必须 | FileNotFoundError 或自定义 | 外部因素导致 |
| 权限不足 | ✅ 必须 | PermissionError 或自定义 | 用户/环境限制 |
| 业务规则违反 | ✅ 必须 | 自定义异常 | 业务语义需要清晰表达 |
| 参数无效 | ✅ 必须 | ValueError/TypeError | 防止错误传播 |
| 内部逻辑检查 | ❌ 避免 | 用 assert | 这是 bug,不是运行时错误 |
| 正常流程分支 | ❌ 避免 | 不应抛异常 | 用 if/else 判断 |
L3 专家层:深入
Python 如何实现 raise
Python 的 raise 语句在字节码层面由 RAISE_VARARGS 指令实现。当 raise 执行时,PVM 会设置异常状态并触发栈展开(stack unwinding)。
raise 的字节码实现:
┌─────────────────────────────────────────────────────────────┐
│ RAISE_VARARGS 指令执行流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ raise ValueError("bad") │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. LOAD_GLOBAL: ValueError(从全局命名空间加载) │ │
│ │ 2. LOAD_CONST: "bad"(加载异常消息) │ │
│ │ 3. CALL: ValueError("bad")(创建异常对象) │ │
│ │ 4. RAISE_VARARGS: 1(抛出异常,1 个参数) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ RAISE_VARARGS 内部步骤: │ │
│ │ │ │
│ │ a. 将异常对象推入栈顶 │ │
│ │ b. 设置当前帧的异常状态 │ │
│ │ c. 开始栈展开(unwind stack) │ │
│ │ - 逐帧弹出,直到找到匹配的 except 块 │ │
│ │ - 每弹出一帧,追加到 traceback │ │
│ │ d. 如果到达栈底仍无 except → 调用 sys.excepthook │ │
│ │ e. 找到 except 块 → 跳转到处理代码 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键点: │
│ • raise 本身只是一个"标记异常并触发展开"的指令 │
│ • traceback 在展开过程中逐步构建,不在 raise 时完成 │
│ • 这就是为什么异常处理比 raise 本身更耗时 │
│ │
└─────────────────────────────────────────────────────────────┘验证代码:
python
import dis
def demo_raise():
raise ValueError("测试异常")
print("demo_raise 字节码:")
dis.dis(demo_raise)
# 输出:
# LOAD_GLOBAL ValueError
# LOAD_CONST '测试异常'
# CALL ...
# RAISE_VARARGS 1
# 查看空的 raise(重新抛出)
def demo_reraise():
try:
raise ValueError("first")
except ValueError:
raise # ← 重新抛出当前异常
print("\ndemo_reraise 字节码:")
dis.dis(demo_reraise)
# raise 单独使用时 RAISE_VARARGS 0(不新建异常,使用当前异常)exception chaining 的底层机制
Python 3 引入的异常链通过 __cause__ 和 __context__ 两个隐藏属性实现:
异常链的 __cause__ vs __context__:
┌─────────────────────────────────────────────────────────────┐
│ 异常链的存储结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ raise NewError("msg") from original_error │
│ │
│ NewError 对象内部: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ __cause__ = original_error ← 显式设置 │ │
│ │ __context__ = original_error ← 由解释器自动设置 │ │
│ │ │ │
│ │ (__cause__ 不为 None 时,打印时会显示: │ │
│ │ "The above exception was the direct cause...") │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ raise NewError("msg") # 在一个 except 块内 │
│ │
│ NewError 对象内部: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ __cause__ = None │ │
│ │ __context__ = original_error ← 由解释器自动设置 │ │
│ │ │ │
│ │ (__cause__ 为 None 但 __context__ 不为 None 时: │ │
│ │ "During handling of above exception, another...") │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ raise NewError("msg") from None │
│ │
│ 显式断开异常链: │
│ __cause__ = None │
│ __context__ = None ← __context__ 被抑制 │
│ │
└─────────────────────────────────────────────────────────────┘验证代码:
python
import sys
import traceback
def demo_chain():
try:
try:
raise ValueError("原始错误")
except ValueError as e:
raise RuntimeError("包装错误") from e
except RuntimeError as wrapped:
print(f"__cause__: {wrapped.__cause__}")
print(f"__context__: {wrapped.__context__}")
print(f"__suppress_context__: {wrapped.__suppress_context__}")
traceback.print_exc()
demo_chain()assert 的内部实现
python
import dis
def demo_assert():
x = -1
assert x > 0, "x 必须为正数"
print("assert 字节码:")
dis.dis(demo_assert)
# assert 编译为:
# LOAD_ASSERTION_ERROR ← Python 3.11+ 的优化指令(直接加载 AssertionError)
# 然后:
# LOAD_GLOBAL AssertionError(3.10 及之前)
# 条件跳转... 如果不满足则 RAISE_VARARGS关键特性:
-O(optimize)标志会移除所有assert语句-OO还会移除 docstring__debug__常量在 Python 启动时根据-O标志设置为True/Falseassert expr等价于if __debug__ and not expr: raise AssertionError
性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| raise(创建异常对象) | O(1) | 异常对象创建是常数时间 |
| raise(栈展开到匹配) | O(d) | d 为需要展开的栈帧数 |
| raise ... from e | O(d) + O(1) | 额外设置 __cause__ 属性 |
| except 多子句匹配 | O(m) | m 为子句数,线性检查 isinstance |
| assert(通过) | O(1) | 条件检查的时间 |
| assert(失败) | O(d) + O(1) | 同 raise + 构建 AssertionError |
性能对比:返回 None vs 抛出异常
python
import timeit
# 方式 1:返回 None 表示错误
def div_return(a, b):
if b == 0:
return None
return a / b
# 方式 2:抛出异常
def div_raise(a, b):
if b == 0:
raise ValueError("除数为零")
return a / b
# 正常路径(无错误)性能对比
normal_return = timeit.timeit("div_return(10, 2)", globals=globals(), number=1000000)
normal_raise = timeit.timeit("div_raise(10, 2)", globals=globals(), number=1000000)
print(f"返回 None(正常路径): {normal_return:.4f}s")
print(f"抛出异常(正常路径): {normal_raise:.4f}s")
# 两者几乎相同 — raise 未执行时无开销
# 错误路径性能对比(用异常但捕获)
error_return = timeit.timeit(
"div_return(10, 0)", globals=globals(), number=100000
)
error_raise = timeit.timeit(
"try: div_raise(10, 0)\nexcept ValueError: pass",
globals=globals(), number=100000
)
print(f"返回 None(错误路径): {error_return:.4f}s") # 快
print(f"抛出异常(错误路径): {error_raise:.4f}s") # 慢 10-100 倍
# 结论:异常适合"真异常"场景,频繁的错误用返回值更高效设计动机
Python 为什么这样设计 raise 和异常系统?
| 设计选择 | 原因 | 替代方案对比 |
|---|---|---|
raise 而非 throw | Python 风格一致性(类似其他关键字) | Java/C++ 用 throw |
raise ... from e 保留原始错误 | 调试时可追溯根本原因 | Java 有 initCause() 实现类似功能 |
assert 可被禁用 | 区分开发检查和生产运行 | C 的 #ifdef NDEBUG 类似 |
| 异常可携带信息(对象) | 比错误码更灵活 | C 用 errno + 全局变量 |
from None 断开异常链 | 安全场合可隐藏内部错误细节 | Java 无等价机制 |
知识关联
raise 与异常链知识关联图:
┌───────────────┐
│ RAISE_VARARGS │
│ 字节码指令 │
└───────────────┘
│
↓
┌─────────────┐ ┌───────────────┐ ┌───────────────┐
│ 栈展开 │────→│ raise │────→│ try-except │
│ unwinding │ │ 异常抛出 │ │ 异常捕获 │
└─────────────┘ └───────────────┘ └───────────────┘
│
↓
┌───────────────┐
│ __cause__ │
│ __context__ │
│ 异常链机制 │
└───────────────┘
│
↓
┌───────────────┐
│ assert │
│ __debug__/_O │
└───────────────┘
│
↓
┌───────────────┐
│ 自定义异常 │
│ 层次化设计 │
└───────────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 抛出异常 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ raise 语句: │
│ ✓ raise ValueError("错误信息") │
│ ✓ raise 重新抛出当前异常 │
│ ✓ raise ... from e 保留原始异常 │
│ │
│ 自定义异常: │
│ ✓ 继承 Exception 类 │
│ ✓ 可以添加自定义属性 │
│ ✓ 可以创建异常层次结构 │
│ │
│ 断言: │
│ ✓ assert condition, "错误信息" │
│ ✓ 用于调试和测试 │
│ ✓ 生产环境可能被禁用 │
│ │
│ 最佳实践: │
│ ✓ 参数验证用异常,逻辑检查用断言 │
│ ✓ 异常信息要清晰具体 │
│ ✓ 建立合理的异常层次结构 │
│ │
└─────────────────────────────────────────────────────────────┘