Python 异常处理实战:从 try/except 到自定义异常
Python 异常处理实战:从 try/except 到自定义异常
写 Python 时,程序崩溃往往不是因为逻辑写错,而是因为没料到的输入、网络抖动或者文件不存在。异常处理的目的不是“让程序永远不报错”,而是让错误在可控的地方发生、可读地记录、可恢复地处理。
一、先理解:异常是流程,不是事故
Python 奉行 EAFP(Easier to Ask for Forgiveness than Permission):先尝试做,失败了再处理,而不是事先把所有条件检查一遍。
# LBYL:先检查,仍有竞态风险
import os
if os.path.exists("data.txt"):
with open("data.txt") as f:
data = f.read()
# EAFP:直接尝试,捕获明确的异常
try:
with open("data.txt") as f:
data = f.read()
except FileNotFoundError:
data = ""
第二种写法更简洁,而且避免了“检查时存在、打开时消失”的竞态问题。
二、try/except/else/finally 的完整形态
四个子句各有分工,记住一句话:except 处理失败,else 只在成功时跑,finally 无论如何都跑。
def load_config(path):
try:
with open(path) as f:
config = f.read()
except FileNotFoundError:
print("配置文件不存在,使用默认配置")
return {}
except PermissionError:
print("没有读取权限")
raise
else:
print("配置读取成功")
return config
finally:
print("load_config 执行完毕")
注意两个细节:一是 except 的顺序要先具体后宽泛,把 FileNotFoundError 写在 Exception 前面,否则具体分支永远走不到;二是 else 里不要放可能抛异常的逻辑,否则异常会被误认为是 try 块里的失败。
三、不要写 bare except,也不要静默吞异常
# 反例:连 KeyboardInterrupt、SystemExit 也会被吞掉
try:
do_something()
except:
pass
# 正例:只捕获你知道如何处理的异常,并留下记录
import logging
try:
do_something()
except ValueError as e:
logging.warning("输入不合法:%s", e)
如果确实需要兜底,至少写 except Exception,并且把异常信息记下来。生产环境里“悄悄失败”比“大声崩溃”更难排查。
四、抛出异常时保留上下文:raise from
在封装底层异常时,用 raise ... from ... 显式建立异常链,排查时能同时看到原始原因和业务语义。
class DataLoadError(Exception):
"""数据加载失败"""
def load_users(path):
try:
return read_json(path)
except (OSError, ValueError) as e:
raise DataLoadError(f"无法加载用户数据:{path}") from e
调用方看到 DataLoadError 就知道是业务层面的问题,展开 __cause__ 又能看到原始的 OSError 或 ValueError。
五、自定义异常:让错误会说话
自定义异常不需要很复杂,关键是按“调用方如何处理”来划分,而不是按“哪里出错”来划分。
class PaymentError(Exception):
pass
class InsufficientBalanceError(PaymentError):
pass
class PaymentTimeoutError(PaymentError):
pass
def pay(amount, balance):
if amount > balance:
raise InsufficientBalanceError("余额不足")
# ... 调用支付网关,超时则 raise PaymentTimeoutError
这样调用方可以只捕获 PaymentError 做统一降级,也可以针对余额不足单独提示用户充值。好的异常层级,就是一份可执行的错误文档。
六、with 语句与异常的配合
with 能保证资源释放,即使块内抛异常也会先执行清理逻辑。标准库的 contextlib.suppress 则适合“这个异常真的可以忽略”的场景:
from contextlib import suppress
# 缓存文件删不掉也无所谓,不必为此写四行 try/except
with suppress(FileNotFoundError):
os.remove("cache.tmp")
但 suppress 只适合你确定可以忽略的异常类型,不要用它包住大段逻辑。
七、自查清单
1. 捕获的是具体异常,而不是 except: 或 except Exception: pass。
2. 异常分支里有日志或明确的降级行为,而不是空着。
3. except 顺序先具体后宽泛,else 只放成功路径。
4. 对外抛出的异常带有业务语义,底层原因用 from 保留。
5. 资源清理交给 with/finally,不要依赖调用方记得关文件、关连接。
异常处理写得好,程序不是“不出错”,而是“出错时依然体面”。把每一次异常都当成一次沟通:告诉调用方发生了什么、严不严重、接下来该怎么办,这就是异常处理的全部要义。
- 点赞
- 收藏
- 关注作者
评论(0)