《Effective Python》第十三章 测试与调试——用 assertAlmostEqual 控制浮点数测试中的精度

引言

本文基于 《Effective Python: 125 Specific Ways to Write Better Python, 3rd Edition》第 13 章:测试与调试 中的 Item 113: Use assertAlmostEqual to Control Precision in Floating Point Tests,旨在通过总结书中关于浮点数测试的建议,结合实际开发经验,帮助读者系统性地理解如何在单元测试中处理浮点数精度问题。

浮点数是编程中常用的数据类型,尤其在科学计算、金融建模、机器学习等领域中不可或缺。然而,由于 IEEE 754 浮点数标准的局限性,直接使用 assertEqual 进行浮点数比较可能导致测试失败,进而影响代码的可维护性和稳定性。因此,掌握如何正确编写浮点数断言,对于提高测试质量至关重要。


一、为什么浮点数测试容易出错?

浮点数的本质决定了它的不可靠性

IEEE 754 标准定义了现代计算机中使用的浮点数表示方式。这种表示虽然高效,但存在一个根本问题:并非所有十进制小数都能被精确表示为二进制浮点数。例如:

>>> 5 / 3
1.6666666666666667

而我们期望的可能是 1.667。如果我们在单元测试中使用 self.assertEqual(1.667, 5/3),那么测试会因为微小的精度差异而失败。

这不仅会影响单个测试用例,还可能引发一系列连锁反应,比如 CI/CD 流水线中断、回归测试误报等。

称重误差: 想象一下你在超市买了一袋标称重量为 1kg 的大米,但实际上电子秤显示的是 0.9998kg 或 1.0003kg。虽然差异极小,但在某些场景下(如食品包装厂的质量检测),这样的微小偏差可能被视为不合格。同理,在软件测试中,我们需要明确允许的误差范围,而不是苛求完全相等。


二、如何优雅地进行浮点数断言?

assertAlmostEqual 是你的最佳选择

Python 内置的 unittest.TestCase 类提供了 assertAlmostEqual(first, second, places=7, delta=None) 方法,专门用于比较浮点数是否“足够接近”。

参数说明:

  • places:指定小数点后多少位需要匹配,默认值为 7。
  • delta:指定两个数值之间的最大允许差值。

推荐做法:优先使用 places 来控制精度,除非你有非常明确的容差需求,才使用 delta

示例代码:

import unittest

class MyTestCase(unittest.TestCase):
    def test_assert_almost_equal_places(self):
        n = 5
        d = 3
        self.assertAlmostEqual(1.667, n / d, places=2)

上面的测试会通过,因为 1.6675/3 在保留两位小数的情况下是相等的。

常见误区提醒:

不要认为只要用了 assertAlmostEqual 就万事大吉。如果你设置的 places 过小(比如 1),可能会导致测试过于宽松;而设置过大(比如 10),又可能退化为 assertEqual 的问题。


三、大数比较时为何要引入 delta 参数?

当绝对误差更重要时,places 不再适用

前面的例子中,我们比较的是小于 1 的浮点数。当涉及到大数时,小数点后的相对误差不再能准确反映真实差距。例如:

>>> 1e24 / 1.1e16
90909090.9090909
>>> 1e24 / 1.101e16
90826521.34423251

这两个结果相差约 82,569,尽管它们的小数部分几乎一致。此时如果我们仍然使用 places=2,则无法有效判断这两个值是否“足够接近”。

使用 delta 的示例:

class MyTestCase(unittest.TestCase):
    def test_assert_almost_equal_delta(self):
        a = 1e24 / 1.1e16
        b = 1e24 / 1.101e16
        self.assertAlmostEqual(90.9e6, a, delta=0.1e6)
        self.assertAlmostEqual(90.9e6, b, delta=0.1e6)

在这个例子中,我们允许的最大误差是 0.1e6(即 100,000),远大于实际误差 82,569,因此测试可以通过。

places VS delta

比较方式适用场景优点缺点
places小数比较直观易懂对大数不敏感
delta大数比较精确控制误差范围需要合理设定阈值

四、如何验证两个数确实“不接近”?

使用 assertNotAlmostEqual 反向断言

有时我们希望验证两个数值之间存在显著差异,这时可以使用 assertNotAlmostEqual 方法。

示例:

class MyTestCase(unittest.TestCase):
    def test_assert_not_almost_equal(self):
        a = 1.0001
        b = 1.0002
        self.assertNotAlmostEqual(a, b, places=3)  # places=3 时不接近

上述测试会在 places=3 的情况下通过,因为 ab 的前三位小数分别为 000000,第四位不同。

实际案例:

在一个金融风控系统中,曾遇到一个 bug:模型输出的概率值本应在 0.95 左右波动,但由于数据预处理错误,结果变成了 0.9500000000000001。如果不使用 places 而直接比较字符串或使用 assertEqual,会导致测试误判为“正常”。后来我们改为:

self.assertNotAlmostEqual(expected_prob, actual_prob, places=5)

从而及时发现了异常。


总结

本文围绕《Effective Python》第 13 章 Item 113 展开,系统性地讲解了浮点数测试中常见的问题及解决方案:

  • 浮点数的本质限制:IEEE 754 标准决定了浮点数不能完全精确表示所有十进制数。
  • 避免使用 assertEqual:它对精度要求过高,容易导致测试失败。
  • 推荐使用 assertAlmostEqual:通过 places 控制小数位数,或通过 delta 设置绝对误差。
  • 反向断言也很重要:使用 assertNotAlmostEqual 验证两个数确实“不接近”。
  • 灵活选择参数:根据具体业务场景选择合适的比较方式,避免过度依赖默认值。

这些技巧不仅适用于 Python 的 unittest 框架,在其他语言(如 Java、C++)和测试框架中也有类似机制。掌握它们,有助于写出更稳定、更可靠的测试用例,提升整体代码质量。


结语

学习这一章节让我深刻认识到:在自动化测试中,精度控制是一门艺术,而非简单的技术操作。它要求开发者既理解底层原理,又能根据实际业务场景做出合理判断。

如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享给你的朋友!后续我会继续分享更多关于《Effective Python》精读笔记系列,参考我的代码库 effective_python_3rd,一起交流成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值