Python 异常处理最佳实践:别再只会 try-except 了

站长 2026-09-29 0 约 2 分钟 424 字
#Python#异常处理#日志#最佳实践

为什么「裸 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)

我的头像

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