25、异常处理与面向对象:自定义异常类,异常链,资源清理与上下文管理

异常处理,说白了就是给程序穿上防弹衣。你写代码的时候,总得考虑“万一出事了怎么办”。我刚开始写Python那会儿,总觉得异常处理是多余的,代码能跑就行呗。直到有一次线上服务因为一个文件没找到直接崩溃了,用户数据丢了半小时……嗯,从那以后我再也不敢轻视异常处理了。

今天咱们聊聊异常处理在面向对象中的高级玩法。不是简单的try-except,而是如何用面向对象的思想,把异常处理做得更优雅、更可控。

25.1 自定义异常类:让错误更有意义

Python内置的异常类型其实够用,但有时候你希望错误信息更具体。比如用户余额不足,你抛个ValueError,别人看了也不知道是啥意思。这时候就该自定义异常类了。

自定义异常类很简单,继承Exception就行。我在项目中遇到过这样一个场景:电商系统的库存扣减,如果库存不足,我希望抛出一个明确的异常,而不是让程序默默返回False。

class InsufficientStockError(Exception):
    """库存不足异常"""
    def __init__(self, sku_id, requested_qty, available_qty):
        self.sku_id = sku_id
        self.requested_qty = requested_qty
        self.available_qty = available_qty
        self.message = f"商品{sku_id}库存不足:请求{requested_qty}件,可用{available_qty}件"
        super().__init__(self.message)

# 使用示例
def deduct_stock(sku_id, qty):
    available = get_stock(sku_id)
    if qty > available:
        raise InsufficientStockError(sku_id, qty, available)
    # 正常扣减逻辑...
我的习惯:自定义异常类名以Error结尾,继承自Exception。别继承BaseException,那玩意儿连系统退出都能捕获,太危险了。

25.2 异常链:追根溯源

有时候一个异常是由另一个异常引发的。比如你读取配置文件,文件不存在,但你希望抛出一个“配置加载失败”的异常,同时保留原始异常信息。这就是异常链的用武之地。

Python 3里用raise ... from ...语法。我建议你养成习惯,只要在except块里抛新异常,就用from把原始异常带上。不然排查问题的时候,你只能看到“配置加载失败”,却不知道是文件不存在还是格式错误。

def load_config(path):
    try:
        with open(path) as f:
            return parse_config(f.read())
    except FileNotFoundError as e:
        raise ConfigLoadError(f"配置文件{path}未找到") from e
    except json.JSONDecodeError as e:
        raise ConfigLoadError(f"配置文件{path}格式错误") from e

# 捕获时可以看到完整链条
try:
    config = load_config("app.json")
except ConfigLoadError as e:
    print(f"错误:{e}")
    print(f"原始原因:{e.__cause__}")
我曾经踩过的坑:在except块里直接raise不带参数,会重新抛出当前异常,这没问题。但如果你写raise Exception("新错误")而不加from,原始异常信息就丢了。调试的时候你会抓狂的。

25.3 资源清理与上下文管理

资源清理是个老生常谈的话题。文件要关闭、数据库连接要释放、锁要解锁。你想想看,如果每个地方都写try-finally,代码得多难看?

Python的上下文管理器(with语句)就是解决这个问题的。你只需要实现__enter____exit__两个方法,就能让你的类支持with语句。

class DatabaseConnection:
    def __init__(self, dsn):
        self.dsn = dsn
        self.conn = None

    def __enter__(self):
        print(f"连接数据库:{self.dsn}")
        self.conn = create_connection(self.dsn)
        return self.conn  # 返回给as后面的变量

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.conn:
            print("关闭数据库连接")
            self.conn.close()
        # 返回False表示不抑制异常,True表示抑制
        return False

# 使用
with DatabaseConnection("mysql://localhost:3306/mydb") as conn:
    conn.execute("SELECT * FROM users")

这里有个细节:__exit__的返回值决定了是否抑制异常。返回True,异常就被吞掉了;返回False,异常继续往上抛。我个人习惯返回False,让异常自然传播,除非你有特殊理由要吞掉它。

25.4 使用contextlib简化上下文管理器

如果你觉得写__enter____exit__太麻烦,Python标准库提供了contextlib模块。用@contextmanager装饰器,你可以用生成器的方式写上下文管理器。

from contextlib import contextmanager

@contextmanager
def database_session(dsn):
    print(f"连接数据库:{dsn}")
    conn = create_connection(dsn)
    try:
        yield conn  # 这里yield出去的就是as后面的值
    finally:
        print("关闭数据库连接")
        conn.close()

# 使用方式完全一样
with database_session("mysql://localhost:3306/mydb") as conn:
    conn.execute("SELECT * FROM users")
我的建议:简单的资源管理用@contextmanager就够了。但如果你的上下文管理器逻辑复杂(比如需要根据异常类型做不同处理),还是老老实实写类吧,可读性更好。

25.5 知识体系总览

下面这张图帮你理清异常处理与面向对象的关系。说白了,核心就三件事:自定义异常让错误更明确、异常链让排查更高效、上下文管理器让资源清理更安全。

异常处理与面向对象核心体系 自定义异常类 • 继承Exception • 添加业务属性 • 自定义错误消息 • 命名以Error结尾 异常链 • raise ... from ... • 保留原始异常 • __cause__属性 • 便于排查问题 上下文管理 • __enter__ / __exit__ • @contextmanager • with语句 • 资源自动清理 三者协同工作流程 1. 自定义异常类 → 定义业务错误类型(如InsufficientStockError) 2. 异常链 → 在上下文管理器中捕获底层异常,包装为业务异常 3. 上下文管理器 → 确保资源释放,异常安全传播 核心思想:让异常处理变得可读、可追踪、可维护

25.6 实战:一个完整的例子

咱们把上面三个知识点串起来,写一个完整的例子。假设你要写一个文件处理服务,需要读取配置文件、连接数据库、处理数据。每一步都可能出错,而且资源必须释放。

class ConfigError(Exception):
    """配置相关异常"""
    pass

class DatabaseError(Exception):
    """数据库相关异常"""
    pass

class DataProcessError(Exception):
    """数据处理异常"""
    pass

class FileProcessor:
    def __init__(self, config_path):
        self.config_path = config_path
        self.config = None
        self.conn = None

    def __enter__(self):
        try:
            self.config = self._load_config()
            self.conn = self._connect_db()
            return self
        except Exception as e:
            self._cleanup()
            raise

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._cleanup()
        return False  # 不抑制异常

    def _load_config(self):
        try:
            with open(self.config_path) as f:
                return json.load(f)
        except FileNotFoundError as e:
            raise ConfigError(f"配置文件{self.config_path}不存在") from e
        except json.JSONDecodeError as e:
            raise ConfigError(f"配置文件格式错误") from e

    def _connect_db(self):
        try:
            return create_connection(self.config['dsn'])
        except Exception as e:
            raise DatabaseError("数据库连接失败") from e

    def _cleanup(self):
        if self.conn:
            self.conn.close()

    def process(self, data):
        try:
            # 处理数据逻辑
            result = self.conn.execute(data)
            return result
        except Exception as e:
            raise DataProcessError("数据处理失败") from e

# 使用
try:
    with FileProcessor("config.json") as processor:
        processor.process(some_data)
except ConfigError as e:
    print(f"配置错误:{e}")
except DatabaseError as e:
    print(f"数据库错误:{e}")
except DataProcessError as e:
    print(f"处理错误:{e}")

关键点总结:

  • 自定义异常让错误类型一目了然
  • 异常链保留了完整的错误追溯路径
  • 上下文管理器确保了资源在任何情况下都能释放
  • 调用方可以按异常类型分别处理,代码更清晰

嗯,这就是异常处理与面向对象结合的精髓。你想想看,如果没有这些机制,你得写多少try-finally?得写多少if-else来判断错误类型?面向对象的思想,说白了就是让代码更贴近现实世界的逻辑。异常也是对象,资源也是对象,用对象的方式去管理它们,一切就顺了。

我的最后一条建议:别滥用自定义异常。如果你的异常只是换个名字,没有增加任何额外信息,那还不如直接用内置的。自定义异常的价值在于携带业务上下文,让调用方能做出更精准的处理。

公众号:蓝海资料掘金营,微信deep3321