Skip to content

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 None

as 关键字的说明

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 result

finally 子句

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 None

finally 的应用场景

┌─────────────────────────────────────────┐
│         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:
从具体到宽泛排序确保具体异常被正确处理ValueErrorException
使用 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 内包含更多代码后续逻辑错误也会被意外捕获
成功逻辑放 elsetry 只做可能失败的操作异常范围明确,后续错误不被掩盖

异常处理反模式

反模式 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 zero

traceback 的作用

┌─────────────────────────────────────────────────────────────────┐
│                    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 特有,解决"信息丢失"问题                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

与其他语言的对比

特性PythonJavaGoC
异常类型对象,可继承对象,checked/unchecked无异常,返回 error整数错误码
错误传播自动传播自动传播手动检查 if err != nil手动检查
强制处理不强制(不捕获终止)checked 必须处理不强制不强制
资源清理finally / withfinallydefer手动清理
异常链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:                                                  │
│   ✓ 总是执行,用于清理资源                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘