3、断言的最佳实践:什么时候用断言?什么时候不该用?

说实话,我见过不少团队在断言的使用上走了极端。要么满屏都是 assert,要么一个 assert 都不敢用。这两种做法,我都踩过坑。

今天我就把这些年总结的经验摊开来讲。什么时候该用断言,什么时候千万别用,咱们一次性说清楚。

3.1 核心原则:断言是「内部契约」,不是「用户输入校验」

先记住一句话:断言是用来检查「不应该发生」的情况,而不是「可能发生」的情况。

什么意思呢?我举个例子你就明白了。

# 错误用法:用断言检查用户输入
def divide(a, b):
    assert b != 0, "除数不能为零!"
    return a / b

# 正确用法:用条件判断处理用户输入
def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零!")
    return a / b

你想想看,用户输入 0 是「可能发生」的事。这不是 bug,是正常业务场景。断言一旦被禁用(比如 python -O 运行时),这个检查就失效了。到时候用户输入 0,程序直接崩溃,那可就出大事了。

⚠️ 我曾经犯过的错: 在一个支付系统里用 assert 检查金额是否为正数。结果线上环境优化性能时加了 -O 参数,导致一笔负数金额通过了校验……嗯,那次事故让我记住了这个教训。

3.2 什么时候该用断言?

我个人习惯把断言用在三个场景里。这些场景都有一个共同点:如果断言失败,说明代码里有 bug,而不是外部环境出了问题。

3.2.1 前置条件检查(Preconditions)

函数内部对参数做「合理性」检查。注意,不是「合法性」检查。

def calculate_discount(price, rate):
    # 前置条件:rate 必须在 0 到 1 之间
    # 如果调用者传了 1.5,那是调用者的 bug
    assert 0 <= rate <= 1, f"折扣率 {rate} 超出范围"
    
    # 前置条件:price 必须是正数
    assert price > 0, f"价格 {price} 不合法"
    
    return price * rate

这里的关键是:rate 和 price 的来源是内部代码。如果传错了,说明上游逻辑有 bug。断言能帮你快速定位问题。

3.2.2 后置条件检查(Postconditions)

函数执行完后,检查结果是否符合预期。这个习惯我是在做金融系统时养成的。

def process_order(order):
    # ... 复杂的订单处理逻辑 ...
    result = do_something(order)
    
    # 后置条件:处理后的订单状态不能是 "pending"
    assert result.status != "pending", f"订单 {order.id} 处理异常"
    
    return result

后置条件检查特别适合那些「理论上不可能出错」的逻辑。一旦出错,说明你的算法或逻辑有漏洞。

3.2.3 不变式检查(Invariants)

这个稍微抽象一点。不变式指的是「在整个过程中必须一直成立的条件」。

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        # 不变式:余额不能为负
        assert self.balance >= 0
    
    def withdraw(self, amount):
        old_balance = self.balance
        self.balance -= amount
        # 不变式:提款后余额不能为负
        assert self.balance >= 0, f"余额不足!当前余额 {old_balance},提款 {amount}"
        return self.balance
💡 我的小技巧: 在复杂的数据结构操作中,我会在关键节点插入不变式检查。比如链表反转后,检查头尾节点是否正确。这些检查在调试阶段帮我抓到了不少隐蔽的 bug。

3.3 什么时候不该用断言?

这部分更重要。我列了几个「禁区」,你最好记下来。

场景 为什么不能用断言 应该用什么
用户输入校验 断言可能被禁用,校验会失效 if + raise ValueError
文件/网络操作 外部环境不可控,断言失败会直接崩溃 try/except 异常处理
数据库操作 数据一致性需要事务保证,不是断言能解决的 事务 + 回滚机制
性能关键路径 断言有性能开销,且无法被优化 日志记录 + 监控告警
第三方接口调用 接口返回结果不可预测 状态码检查 + 重试机制

说白了,凡是「外部输入」或「环境因素」导致的问题,都不该用断言。断言只负责抓「内部 bug」。

3.4 实战中的断言策略

我一般会按「风险等级」来分层使用断言。这里画个图,你一看就明白了。

断言使用策略金字塔 高风险:必须用断言 前置条件、后置条件、不变式 中风险:建议用断言 内部接口参数校验、算法中间结果检查 低风险:谨慎用断言 调试辅助、单元测试中的临时检查 禁用区:不用断言 用户输入、文件IO、网络请求、数据库操作

这个金字塔模型是我在实际项目中慢慢总结出来的。顶层是「必须用」的,底层是「不用」的。中间两层看情况灵活处理。

3.5 几个实用建议

最后,分享几个我个人的小习惯。不一定适合所有人,但你可以参考一下。

  1. 断言信息要写清楚:别只写个 "assert x",要带上错误信息。比如 assert x > 0, f"x 的值为 {x},期望大于 0"。这样出问题时一眼就能看出问题在哪。
  2. 不要用断言做数据清理:断言失败就崩溃了,没有机会做清理工作。如果你需要在失败时释放资源,用 try/finally。
  3. 测试代码里多用断言:单元测试是断言的最佳舞台。pytest 的 assert 就是专门干这个的。
  4. 生产环境慎用断言:如果一定要在生产环境用,确保你的部署脚本不会加 -O 参数。我一般会在 CI/CD 流程里检查这个。

核心总结:

  • 断言是「内部契约」,不是「外部校验」
  • 用断言抓「内部 bug」,用异常处理「外部异常」
  • 高风险逻辑必须用断言,外部交互不用
  • 断言信息要详细,方便快速定位问题

嗯,关于断言的最佳实践,差不多就这些了。记住一句话:断言是开发者的朋友,但不是万能工具。用对了地方,它能帮你省下大量调试时间;用错了地方,它可能给你惹来大麻烦。

可可精品资料库