浮点数是一种快速有效的存储和处理数字的方法,但它们带来了一系列陷阱,这些陷阱肯定会困扰许多初出茅庐的程序员——也许还有一些有经验的程序员!展示浮点数陷阱的经典示例如下:
>>> 0.1 + 0.2 == 0.3 False
第一次看到这个可能会迷失方向。但是不要把你的电脑扔进垃圾桶。这种行为是正确的!
本文将向您展示为什么像上面这样的浮点错误很常见,为什么它们有意义,以及在 Python 中可以做些什么来处理它们。
你的电脑是骗子(有点)
您已经看到0.1 + 0.2
不等于0.3
,但疯狂并不止于此。以下是一些更令人困惑的例子:
>>> 0.2 + 0.2 + 0.2 == 0.6 False >>> 1.3 + 2.0 == 3.3 False >>> 1.2 + 2.4 + 3.6 == 7.2 False
问题不仅限于相等比较:
>>> 0.1 + 0.2 <= 0.3 False >>> 10.4 + 20.8 > 31.2 True >>> 0.8 - 0.1 > 0.7 True
发生什么了?你的电脑在骗你吗?它确实看起来像,但在表面之下还有更多的事情发生。
当您在 Python 解释器中输入数字0.1
时,它会以浮点数的形式存储在内存中。发生这种情况时会发生转换。 0.1
是以 10 为底的十进制数,但浮点数以二进制形式存储。换句话说, 0.1
从以 10 为底转换为以 2 为底。
生成的二进制数可能无法准确地表示原始的以 10 为基数的数字。 0.1
就是一个例子。二进制表示是 \(0.0\overline{0011}\)。也就是说, 0.1
在以 2 为底数时是无限重复的小数。当您将分数 ⅓ 作为以 10 为底数的小数时,也会发生同样的情况。您最终会得到无限重复的小数 \(0.\overline{33}\ )。
计算机内存是有限的,因此0.1
的无限重复二进制小数表示被四舍五入为有限小数。此数字的值取决于您计算机的体系结构(32 位与 64 位)。查看为0.1
存储的浮点值的一种方法是使用浮点数的.as_integer_ratio()
方法来获取浮点表示的分子和分母:
>>> numerator, denominator = (0.1).as_integer_ratio() >>> f"0.1 ≈ {numerator} / {denominator}" '0.1 ≈ 3602879701896397 / 36028797018963968'
现在使用format()
显示精确到小数点后 55 位的分数:
>>> format(numerator / denominator, ".55f") '0.1000000000000000055511151231257827021181583404541015625'
所以0.1
被四舍五入到一个比它的真实值稍大的数字。
.as_integer_ratio()
等数字方法的更多信息。这个错误,称为浮点表示错误,发生的频率比你想象的要多。
表示错误真的很常见
在表示为浮点数时,数字会被四舍五入的三个原因:
- 该数字具有比浮点允许的更多有效数字。
- 这个数字是不合理的。
- 该数字是有理数,但具有非终止二进制表示。
64 位浮点数适用于大约 16 或 17 位有效数字。任何具有更高有效数字的数字都会被四舍五入。无理数,如 π 和e ,不能由任何整数基数中的任何终止分数表示。同样,无论如何,无理数在存储为浮点数时都会四舍五入。
这两种情况会产生一组无法精确表示为浮点数的无限数字。但除非你是处理微小数字的化学家,或者是处理天文数字的物理学家,否则你不太可能遇到这些问题。
非终止有理数怎么样,比如以 2 为底的0.1
?这是您会遇到大多数浮点问题的地方,并且由于确定分数是否终止的数学运算,您将比您想象的更频繁地遇到表示错误。
在以 10 为底的情况下,如果分数的分母是 10 的素因数的幂的乘积,则分数可以表示为终止分数。10 的两个素因数是 2 和 5,因此像 ½、¼、⅕、⅛ 和⅒ 全部终止,但 ⅓、⅐ 和 ⅑ 不终止。然而,在以 2 为底的情况下,只有一个质因数:2。所以只有分母是 2 的幂的分数才会终止。因此,像⅓、⅕、⅙、⅐、⅑和⅒这样的分数在用二进制表示时都是不终止的。
您现在可以理解本文中的原始示例。 0.1
、 0.2
和0.3
都在转换为浮点数时四舍五入:
>>> # -----------vvvv Display with 17 significant digits >>> format(0.1, ".17g") '0.10000000000000001' >>> format(0.2, ".17g") '0.20000000000000001' >>> format(0.3, ".17g") '0.29999999999999999'
当0.1
和0.2
相加时,结果是一个略大于0.3
的数字:
>>> 0.1 + 0.2 0.30000000000000004
由于0.1 + 0.2
略大于0.3
并且0.3
由一个略小于自身的数字表示,因此表达式0.1 + 0.2 == 0.3
的计算结果为False
。
0.1 + 0.2
的结果。如何在 Python 中比较浮点数
那么,在 Python 中比较浮点数时如何处理浮点表示错误呢?诀窍是避免检查相等性。切勿将==
、 >=
或<=
与浮点数一起使用。请改用math.isclose()
函数:
>>> import math >>> math.isclose(0.1 + 0.2, 0.3) True
math.isclose()
检查第一个参数是否可以接受地接近第二个参数。但这究竟是什么意思?诀窍是检查第一个参数和第二个参数之间的距离,这相当于两个值之差的绝对值:
>>> a = 0.1 + 0.2 >>> b = 0.3 >>> abs(a - b) 5.551115123125783e-17
如果abs(a - b)
小于a
或b
中较大者的某个百分比,则认为a
足够接近b
以“等于” b
。这个百分比称为相对容差。您可以使用math.isclose()
的rel_tol
关键字参数指定它,默认为1e-9
。换句话说,如果abs(a - b)
小于0.00000001 * max(abs(a), abs(b))
,则认为a
和b
彼此“接近”。这保证了a
和b
大约等于九位小数。
如果需要,您可以更改相对容差:
>>> math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-20) False
当然,相对容差取决于您要解决的问题所设置的约束。然而,对于大多数日常应用,默认的相对容差就足够了。
但是,如果a
或b
之一为零且rel_tol
小于 1,则会出现问题。在这种情况下,无论非零值与零有多接近,相对容差都会保证接近性检查始终失败。在这种情况下,使用绝对容差作为后备:
>>> # Relative check fails! >>> # ---------------vvvv Relative tolerance >>> # ----------------------vvvvv max(0, 1e-10) >>> abs(0 - 1e-10) < 1e-9 * 1e-10 False >>> # Absolute check works! >>> # ---------------vvvv Absolute tolerance >>> abs(0 - 1e-10) < 1e-9 True
math.isclose()
将自动为您执行此检查。 abs_tol
关键字参数确定绝对容差。但是, abs_tol
默认为0.0
,因此如果您需要检查值与零的接近程度,则需要手动设置。
总而言之, math.isclose()
返回以下比较的结果,它将相对和绝对测试组合成一个表达式:
abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
math.isclose()
是在PEP 485中引入的,并且从 Python 3.5 开始可用。
什么时候应该使用math.isclose()
?
通常,只要需要比较浮点值,就应该使用math.isclose()
。将==
替换为math.isclose()
:
>>> # Don't do this: >>> 0.1 + 0.2 == 0.3 False >>> # Do this instead: >>> math.isclose(0.1 + 0.2, 0.3) True
您还需要小心>=
和<=
比较。使用math.isclose()
分别处理相等性,然后检查严格比较:
>>> a, b, c = 0.1, 0.2, 0.3 >>> # Don't do this: >>> a + b <= c False >>> # Do this instead: >>> math.isclose(a + b, c) or (a + b < c) True
存在math.isclose()
的各种替代方案。如果你使用 NumPy,你可以利用numpy.allclose()
和numpy.isclose()
:
>>> import numpy as np >>> # Use numpy.allclose() to check if two arrays are equal >>> # to each other within a tolerance. >>> np.allclose([1e10, 1e-7], [1.00001e10, 1e-8]) False >>> np.allclose([1e10, 1e-8], [1.00001e10, 1e-9]) True >>> # Use numpy.isclose() to check if the elements of two arrays >>> # are equal to each other within a tolerance >>> np.isclose([1e10, 1e-7], [1.00001e10, 1e-8]) array([ True, False]) >>> np.isclose([1e10, 1e-8], [1.00001e10, 1e-9]) array([ True, True])
请记住,默认的相对和绝对公差与math.isclose()
不同。 numpy.allclose()
和numpy.isclose()
的默认相对容差为1e-05
,两者的默认绝对容差为1e-08
。
math.isclose()
对于单元测试特别有用,尽管有一些替代方法。 Python 的内置unittest
模块有一个unittest.TestCase.assertAlmostEqual()
方法。但是,该方法仅使用绝对差异检验。这也是一个断言,这意味着失败会引发AssertionError
,使其不适合在您的业务逻辑中进行比较。
用于单元测试的 math.isclose( math.isclose()
的一个很好的替代方法是pytest
包中的pytest.approx()
函数。与math.isclose()
一样, pytest.approx()
接受两个参数并返回它们是否在某个容差范围内相等:
>>> import pytest >>> pytest.approx(0.1 + 0.2, 0.3) True
就像math.isclose()
, pytest.approx()
有rel_tol
和abs_tol
关键字参数用于设置相对和绝对公差。但是,默认值是不同的。 rel_tol
的默认值为1e-6
, abs_tol
的默认值为1e-12
。
如果传递给pytest.approx()
的前两个参数类似于数组,这意味着它们是 Python 可迭代的,如列表或元组,甚至是 NumPy 数组,那么pytest.approx()
的行为类似于numpy.allclose()
并返回两个数组在公差范围内是否相等:
>>> import numpy as np >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == pytest.approx(np.array([0.3, 0.6])) True
pytest.approx()
甚至可以使用字典值:
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == pytest.approx({'a': 0.3, 'b': 0.6}) True
浮点数非常适合在不需要绝对精度时处理数字。它们速度快,内存效率高。但是,如果您确实需要精度,那么您应该考虑一些浮点数的替代方案。
精确的浮点替代方案
Python 中有两种内置的数字类型,它们为浮点数不足的情况提供全精度: Decimal
和Fraction
。
Decimal
类型
Decimal
类型可以根据需要以尽可能高的精度精确存储十进制值。默认情况下, Decimal
保留 28 个有效数字,但您可以将其更改为适合您正在解决的特定问题的任何内容:
>>> # Import the Decimal type from the decimal module >>> from decimal import Decimal >>> # Values are represented exactly so no rounding error occurs >>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3") True >>> # By default 28 significant figures are preserved >>> Decimal(1) / Decimal(7) Decimal('0.1428571428571428571428571429') >>> # You can change the significant figures if needed >>> from decimal import getcontext >>> getcontext().prec = 6 # Use 6 significant figures >>> Decimal(1) / Decimal(7) Decimal('0.142857')
您可以在Python 文档中阅读有关Decimal
类型的更多信息。
Fraction
类型
浮点数的另一种替代方法是Fraction
类型。 Fraction
可以准确地存储有理数并克服浮点数遇到的表示错误问题:
>>> # import the Fraction type from the fractions module >>> from fractions import Fraction >>> # Instantiate a Fraction with a numerator and denominator >>> Fraction(1, 10) Fraction(1, 10) >>> # Values are represented exactly so no rounding error occurs >>> Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10) True
Fraction
和Decimal
标准浮点值提供了许多好处。然而,这些好处是有代价的:速度降低和内存消耗增加。如果您不需要绝对精度,最好坚持使用浮点数。但是对于金融和任务关键型应用程序, Fraction
和Decimal
的权衡可能是值得的。
结论
浮点值既是福也是祸。它们以不准确的表示为代价提供快速的算术运算和高效的内存使用。在本文中,您了解到:
- 为什么浮点数不精确
- 为什么浮点表示错误很常见
- 如何在 Python 中正确比较浮点值
- 如何使用 Python 的
Fraction
和Decimal
类型精确表示数字
如果你学到了一些新东西,那么你可能对 Python 中的数字不了解更多。例如,你知道int
类型不是 Python 中唯一的整数类型吗?在我的文章3 Things You Might Not Know About Numbers in Python 中了解其他整数类型是什么以及其他关于数字的鲜为人知的事实。
其他资源
想将您的 Python 技能提升到一个新的水平吗?我为 Python 编程和技术写作提供一对一的私人辅导。单击此处了解更多信息。
来源: https://davidamos.dev/the-right-way-to-compare-floats-in-python/