首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 10 章 异常处理

10.1 异常体系结构

Python 的异常是一个继承树,根节点是 BaseException

BaseException
├── SystemExit              # sys.exit() 触发
├── KeyboardInterrupt       # Ctrl+C
├── GeneratorExit           # 生成器关闭
└── Exception               # 所有"正常"异常的基类
    ├── StopIteration
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── OverflowError
    │   └── FloatingPointError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError (IOError)
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── ConnectionError
    ├── ValueError
    ├── TypeError
    ├── AttributeError
    ├── NameError
    ├── ImportError
    │   └── ModuleNotFoundError
    ├── RuntimeError
    │   └── RecursionError
    └── ...

重要原则:通常只捕获 Exception 的子类,不要捕获 BaseException(会吞掉 Ctrl+C)。

# 常见异常
int("abc")                  # ValueError
1 / 0                       # ZeroDivisionError
[1, 2, 3][10]              # IndexError
{"a": 1}["b"]              # KeyError
open("不存在.txt")          # FileNotFoundError
"hello" + 5                 # TypeError
x = undefined_var           # NameError
import nonexistent_module   # ModuleNotFoundError

10.2 try / except / else / finally

try:
    result = 10 / int(input("输入除数: "))
except ZeroDivisionError:
    print("不能除以零")
except ValueError:
    print("请输入有效数字")
except Exception as e:
    print(f"未知错误: {e}")
else:
    print(f"结果是: {result}")    # 没有异常时执行
finally:
    print("无论如何都会执行")     # 总是执行(清理代码)

各子句的角色

try:
    # 可能出错的代码
    pass
except SomeError:
    # 处理特定异常
    pass
else:
    # 没有异常时执行
    # 把不需要异常保护的代码放这里,而不是 try 块中
    pass
finally:
    # 无论如何都执行 — 清理资源
    # 即使有 return、break、continue 也会执行
    pass

为什么用 else?

# 不好 — try 块太大,可能捕获意外异常
try:
    data = json.loads(text)
    process(data)  # 如果这里也抛 ValueError,会被错误捕获
except ValueError:
    print("JSON 解析失败")

# 好 — 精确捕获
try:
    data = json.loads(text)
except ValueError:
    print("JSON 解析失败")
else:
    process(data)  # 只有解析成功才执行

10.3 捕获多个异常与异常链

多个异常

# 分别处理
try:
    value = my_dict[key]
except KeyError:
    print("键不存在")
except TypeError:
    print("键类型错误")

# 合并处理
try:
    value = my_dict[key]
except (KeyError, TypeError) as e:
    print(f"访问失败: {e}")

# 获取异常信息
try:
    1 / 0
except ZeroDivisionError as e:
    print(type(e))       # <class 'ZeroDivisionError'>
    print(e)             # division by zero
    print(e.args)        # ('division by zero',)

异常链(Exception Chaining)

# 隐式异常链 — 在处理异常时又发生新异常
try:
    1 / 0
except ZeroDivisionError:
    raise ValueError("计算失败")
# 输出会同时显示两个异常:
# ZeroDivisionError: division by zero
# During handling of the above exception, another exception occurred:
# ValueError: 计算失败

# 显式异常链 — raise ... from ...
try:
    data = json.loads(bad_json)
except json.JSONDecodeError as e:
    raise ValueError("配置文件格式错误") from e
# ValueError: 配置文件格式错误
# The above exception was the direct cause of the following exception:
# json.JSONDecodeError: ...

# 禁用异常链
try:
    1 / 0
except ZeroDivisionError:
    raise ValueError("计算失败") from None
# 只显示 ValueError,隐藏原始异常

10.4 自定义异常类

# 基本自定义异常
class AppError(Exception):
    """应用程序基础异常"""
    pass

class ValidationError(AppError):
    """数据验证异常"""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class NotFoundError(AppError):
    """资源未找到异常"""
    def __init__(self, resource, resource_id):
        self.resource = resource
        self.resource_id = resource_id
        super().__init__(f"{resource}(id={resource_id}) 不存在")

# 使用
def create_user(name, age):
    if not name:
        raise ValidationError("name", "不能为空")
    if age < 0 or age > 150:
        raise ValidationError("age", f"无效的年龄: {age}")
    return {"name": name, "age": age}

try:
    user = create_user("", 25)
except ValidationError as e:
    print(f"验证失败 - 字段: {e.field}, 消息: {e.message}")

异常层级设计

# 好的异常层级
class DatabaseError(Exception): pass
class ConnectionError(DatabaseError): pass
class QueryError(DatabaseError): pass
class IntegrityError(QueryError): pass

# 调用者可以按需捕获不同粒度
try:
    db.execute(query)
except IntegrityError:
    print("数据完整性错误")      # 最具体
except QueryError:
    print("查询错误")            # 较通用
except DatabaseError:
    print("数据库错误")          # 最通用

10.5 异常组与 except*(Python 3.11+)

处理同时发生的多个异常,常见于并发场景:

# 创建异常组
eg = ExceptionGroup("多个错误", [
    ValueError("值错误"),
    TypeError("类型错误"),
    KeyError("键错误"),
])

# except* 可以按类型匹配异常组中的异常
try:
    raise eg
except* ValueError as e:
    print(f"处理 ValueError: {e.exceptions}")
except* TypeError as e:
    print(f"处理 TypeError: {e.exceptions}")
except* KeyError as e:
    print(f"处理 KeyError: {e.exceptions}")
# 实际应用:并发任务中的异常处理
import asyncio

async def task(n):
    if n == 2:
        raise ValueError(f"任务 {n} 失败")
    if n == 4:
        raise TypeError(f"任务 {n} 类型错误")
    return n * 10

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(task(i)) for i in range(5)]
    except* ValueError as eg:
        for e in eg.exceptions:
            print(f"值错误: {e}")
    except* TypeError as eg:
        for e in eg.exceptions:
            print(f"类型错误: {e}")

10.6 上下文管理器与异常抑制

from contextlib import suppress, contextmanager

# suppress — 安全忽略特定异常
with suppress(FileNotFoundError):
    os.remove("可能不存在的文件.txt")
# 等价于
try:
    os.remove("可能不存在的文件.txt")
except FileNotFoundError:
    pass

# 自定义上下文管理器处理异常
@contextmanager
def handled_errors(*exceptions):
    try:
        yield
    except exceptions as e:
        print(f"已处理异常: {e}")

with handled_errors(ValueError, TypeError):
    int("not a number")
# 输出: 已处理异常: invalid literal for int() with base 10: 'not a number'

10.7 LBYL vs EAFP 编程风格

两种处理错误的哲学:

LBYL — Look Before You Leap(三思而后行)

# 先检查再操作
if key in my_dict:
    value = my_dict[key]
else:
    value = default

if os.path.exists(filepath):
    with open(filepath) as f:
        data = f.read()

EAFP — Easier to Ask Forgiveness than Permission(先做再说)

# 先操作,出错再处理 — Python 推荐的风格
try:
    value = my_dict[key]
except KeyError:
    value = default

try:
    with open(filepath) as f:
        data = f.read()
except FileNotFoundError:
    data = None

为什么 Python 偏好 EAFP?

  1. 更快:正常路径不需要额外检查(异常是特殊情况)
  2. 更安全:避免 TOCTOU(检查和使用之间状态变化)竞态条件
  3. 更简洁:减少条件分支嵌套
  4. 鸭子类型友好:不需要检查类型,直接用,不行再说

当然,也不是绝对的。简单的条件判断(如 if x is not None)用 LBYL 更直观。

10.8 warnings 模块

警告不是异常,不会中断程序执行:

import warnings

# 发出警告
warnings.warn("这个函数即将废弃", DeprecationWarning)
warnings.warn("性能可能较差", RuntimeWarning)

# 自定义警告
class SecurityWarning(UserWarning):
    pass

warnings.warn("使用了弱密码", SecurityWarning)

# 控制警告行为
warnings.filterwarnings("ignore", category=DeprecationWarning)  # 忽略
warnings.filterwarnings("error", category=RuntimeWarning)        # 转为异常

# 临时控制
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    # 这里的警告会被忽略
    warnings.warn("被忽略的警告")

在 deprecation 中的使用

import warnings
from functools import wraps

def deprecated(message=""):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(
                f"{func.__name__} 已废弃。{message}",
                DeprecationWarning,
                stacklevel=2,
            )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@deprecated("请使用 new_function() 代替")
def old_function():
    return "old"

old_function()
# DeprecationWarning: old_function 已废弃。请使用 new_function() 代替

本章小结:异常处理是写出健壮代码的关键。遵循 EAFP 风格、精确捕获异常(不要裸 except)、善用 elsefinally、设计合理的异常层级是核心原则。Python 3.11 的异常组为并发场景下的错误处理提供了优雅的解决方案。