在Python中判断两个浮点数的相等

如何判断两个浮点数是否相等? 这是一个所有编程语言都有的小坑。

在其它编程语言,使用==来判断是绝对的大忌,因为浮点数在低位会丢失精度。 无论float还是double都精度有限,只是精度不同。 Python中虽然统一了floatdouble,但不可避免地仍然有这个问题。

精度丢失

>>> 0.1 + 0.2
0.30000000000000004

简单地说,$0.1$在二进制中,是一个无限循环小数$0.0\dot{0}\dot{0}\dot{1}\dot{1}$。 这虽然违反了习惯十进制的人类直觉,但确实是真实存在。 就像$1 \over 3$在十进制中是无限循环小数$0.\dot{3}$一样,而$1 \over 3$在三进制中,表示为$0.1$。 计算机在处理小数时,是通过二进制寄存器来存储和计算,长度有限,因此会丢失无限循环小数低位的数字。

所以,不同进制在转换过程中,可能存在失真。 表现为,小数的低位不准。 那么浮点数的精度如何?

>>> import sys
>>> sys.float_info.epsilon
2.220446049250313e-16

按照C99的float.h,精度大概是这个情况。 前面0.1 + 0.2的那个尾巴是4e-17,小于2.22e-16,在精度允许的范围内。 大部分语言的浮点数遵循这个标准。

所以,如果两个浮点数的差,小于这个精度,就可以认为两个数相等。

def eq(a: float, b: float) -> bool:
    return abs(a - b) < sys.float_info.epsilon

这也是大部分语言判断浮点数相等的手法。 有时,我们也会容忍epsilon大一点。

math.isclose

在Python 3.5以后,通常使用math.isclose来判断两个浮点数相等。

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)

通过查阅文档和测试,发现isclose函数的实现相当于以下代码:

def isclose(a, b, rel_tol=1e-09, abs_tol=0):
    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

相对精度rel_tol普遍适用的,相当于默认保留9位有效数字。 但有时也会有一些反直觉的现象:

>>> math.isclose(1e10, 1e10 + 1)
True

某些领域,或者计算机日常业务的多数场景,使用abs_tol会更符合直觉一些。

>>> math.isclose(1e10, 1e10 + 1, rel_tol=0, abs_tol=0.001)
False
>>> math.isclose(1, 1.01, rel_tol=0, abs_tol=0.001)
False
>>> math.isclose(1, 1.001, rel_tol=0, abs_tol=0.001)
True

不要直接利用==

在Python中,==在判断浮点数的时候,看上去好像实现了math.isclose(a, b, rel_tol=1e-16, abs_tol=0)

>>> 1.0000000000001 == 1
False
>>> 1.00000000000001 == 1
True

其实并没有。因为:

>>> 1.0000000000001
1.0000000000001
>>> 1.00000000000001
1.0

1 + 1e-16只是精度丢失了而已。 Python判断两个浮点数是否相等,底层仍然是C语言实现的,判断的是寄存器的相等性。

MyFloat

但是,通过重载==,可以实现math.isclose的功能,看上去简洁一些。

class MyFloat(float):
    def __eq__(self, other):
        return math.isclose(self, other, rel_tol=0, abs_tol=0.001)

在使用时:

>>> a == MyFloat(0)
>>> a
0.0
>>> a == 0.001
True
>>> 0.001 == a
True

也只是看上去简洁。

参考


相关笔记