Skip to content

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不要继承 BaseExceptionclass MyError(Exception)
命名以 Error/Exception 结尾符合 Python 惯例ValidationErrorDatabaseError
提供清晰的错误信息消息要能帮助定位问题f"字段 '{field}' 不能为空"
携带上下文属性便于程序化处理self.field = field
建立层次结构便于统一处理一类错误DatabaseErrorConnectionError
提供 __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/False
  • assert expr 等价于 if __debug__ and not expr: raise AssertionError

性能考量

操作时间复杂度说明
raise(创建异常对象)O(1)异常对象创建是常数时间
raise(栈展开到匹配)O(d)d 为需要展开的栈帧数
raise ... from eO(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 而非 throwPython 风格一致性(类似其他关键字)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, "错误信息"                            │
│   ✓ 用于调试和测试                                          │
│   ✓ 生产环境可能被禁用                                      │
│                                                             │
│   最佳实践:                                                 │
│   ✓ 参数验证用异常,逻辑检查用断言                          │
│   ✓ 异常信息要清晰具体                                      │
│   ✓ 建立合理的异常层次结构                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘