为什么「裸 except」是灾难
先看新手最常写的代码:
try:
data = json.loads(resp.text)
save_to_db(data)
except Exception:
pass # 出错了?吞掉,假装没事
这段代码的问题是:任何错误都被静默吞掉——JSON 解析失败、数据库连接断开、磁盘写满、甚至代码里的拼写错误,全都悄无声息。出问题时你没有任何线索,只能对着「数据怎么没存进去」发呆两小时。
异常处理的核心原则只有一条:
只捕获你能处理的异常,并且处理时必须留下痕迹。
Python 异常体系速览
异常是有继承层次的,常用的几个:
BaseException
├── KeyboardInterrupt # Ctrl+C
├── SystemExit # sys.exit()
└── Exception # 日常说的「异常」都继承它
├── ValueError # 值不对:int("abc")
├── TypeError # 类型不对:"1" + 1
├── KeyError # 字典键不存在
├── IndexError # 列表下标越界
├── FileNotFoundError # 文件不存在
├── ConnectionError # 网络连接失败
└── TimeoutError # 超时
了解层次的意义在于:捕获父类会连带捕获所有子类。except OSError 会同时接住 FileNotFoundError、PermissionError 等一堆子类。
正确姿势一:精准捕获
try:
with open("config.json", encoding="utf-8") as f:
config = json.load(f)
port = int(config["port"])
except FileNotFoundError:
print("配置文件不存在,使用默认配置")
config = {"port": 8000}
port = 8000
except json.JSONDecodeError as e:
print(f"配置文件格式错误:{e}")
raise # 处理不了就继续抛,别硬撑
except KeyError:
print("配置文件缺少 port 字段")
raise
每个 except 只处理自己能搞定的场景,搞不定的用 raise 原样抛出,让上层或顶层去处理。
正确姿势二:try 块越小越好
# 差:整个流程都包进去,出错时根本不知道是哪一步
try:
data = download()
cleaned = clean(data)
save(cleaned)
notify()
except Exception as e:
print(e)
# 好:每个步骤单独处理,错误定位精确到环节
data = download() # 下载失败应该直接崩溃报错,不用包
try:
cleaned = clean(data)
except ValueError as e:
print(f"数据清洗失败:{e}")
return
save(cleaned)
try 块里只放真正可能抛出你所捕获异常的那几行。
正确姿势三:善用 else 和 finally
完整的异常处理有四个块:
try:
f = open("data.txt", encoding="utf-8")
except FileNotFoundError:
print("文件不存在")
else:
# 没有异常时执行——把成功后的逻辑放这里,职责更清晰
content = f.read()
f.close()
finally:
# 无论是否异常都执行——释放资源的专用位置
print("清理工作")
finally 的典型用途:关文件、关数据库连接、释放锁。(不过现代 Python 更推荐 with 上下文管理器,它内部就是用 finally 实现的。)
正确姿势四:自定义异常
业务逻辑的错误,不要只会 raise Exception("xxx"),定义自己的异常类型:
class BusinessError(Exception):
"""业务异常基类"""
pass
class InsufficientBalanceError(BusinessError):
"""余额不足"""
def __init__(self, balance: float, amount: float):
super().__init__(f"余额不足:当前 {balance} 元,需支付 {amount} 元")
self.balance = balance
self.amount = amount
def withdraw(balance: float, amount: float) -> float:
if amount > balance:
raise InsufficientBalanceError(balance, amount)
return balance - amount
# 调用方可以精准处理
try:
withdraw(100, 200)
except InsufficientBalanceError as e:
print(f"支付失败:{e}")
# e.balance 和 e.amount 还能拿出来做后续处理
好处:调用方能区分「业务错误」(余额不足,提示用户)和「系统错误」(数据库挂了,报警给运维)。
正确姿势五:记录日志而不是 print
程序上服务器后,print 基本等于没写。标准做法:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[
logging.FileHandler("app.log", encoding="utf-8"), # 写文件
logging.StreamHandler(), # 同时输出到控制台
],
)
logger = logging.getLogger(__name__)
try:
result = process(data)
except ValueError as e:
logger.warning(f"数据格式问题:{e}")
except Exception:
# 意料外的异常,用 exception 方法记录完整堆栈
logger.exception("处理数据时发生未知错误")
raise
logger.exception() 是关键——它会把完整的错误堆栈(哪一行、调用链)都记进日志,比 logger.error(str(e)) 信息量多十倍。
正确姿势六:失败重试
网络请求、数据库连接这类「可能暂时性失败」的操作,加重试:
import time
import requests
def fetch_with_retry(url: str, times: int = 3, delay: float = 1.0):
"""带重试的请求:指数退避"""
for attempt in range(1, times + 1):
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
return resp
except requests.RequestException as e:
logger.warning(f"第 {attempt}/{times} 次请求失败:{e}")
if attempt == times:
raise # 重试耗尽,抛出最后一次异常
time.sleep(delay * attempt) # 每次等更久:1s、2s、3s
哪些错误不该重试:参数错误(400)、权限不足(403)、资源不存在(404)——这些是确定性错误,重试一万次也没用,直接抛。
一张决策表
遇到异常时问自己三个问题:
| 问题 | 答案 | 做法 |
|---|---|---|
| 这个错误我能处理吗? | 能(如文件不存在→用默认值) | except 住,处理,继续 |
| 不能 | 不捕获或 raise 抛给上层 | |
| 是暂时性故障吗? | 是(网络抖动) | 重试 + 间隔 |
| 否(参数错误) | 立即失败 | |
| 用户需要知道吗? | 需要 | 转成友好的中文提示 |
| 不需要 | 记日志即可 |
写在最后
异常处理的本质不是「防止程序崩溃」,而是让程序崩溃时留下足够的信息,并且在能恢复的地方优雅恢复。记住三条铁律:精准捕获、绝不静默、日志留痕。做到这三点,半夜被报警叫醒的次数会少很多。







评论 (0)
还没有评论,快来抢沙发吧~