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)
# 正常扣减逻辑...
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__}")
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 知识体系总览
下面这张图帮你理清异常处理与面向对象的关系。说白了,核心就三件事:自定义异常让错误更明确、异常链让排查更高效、上下文管理器让资源清理更安全。
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