引言
本文基于 《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.667
和 5/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
的情况下通过,因为 a
和 b
的前三位小数分别为 000
和 000
,第四位不同。
实际案例:
在一个金融风控系统中,曾遇到一个 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,一起交流成长!