Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

在 Python 中比较浮点数的正确方法

Posted on 2022-03-22

在 Python 中比较浮点数的正确方法

浮点数是一种快速有效的存储和处理数字的方法,但它们带来了一系列陷阱,这些陷阱肯定会困扰许多初出茅庐的程序员——也许还有一些有经验的程序员!展示浮点数陷阱的经典示例如下:

 >>> 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被四舍五入到一个比它的真实值稍大的数字。

?
在我的文章3 Things You Might Not Know About Numbers in Python中了解有关.as_integer_ratio()等数字方法的更多信息。

这个错误,称为浮点表示错误,发生的频率比你想象的要多。

表示错误真的很常见

在表示为浮点数时,数字会被四舍五入的三个原因:

  1. 该数字具有比浮点允许的更多有效数字。
  2. 这个数字是不合理的。
  3. 该数字是有理数,但具有非终止二进制表示。

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 。

❗
浮点表示错误是每种语言的每个程序员都需要了解并知道如何处理的问题。它不是特定于 Python 的。您可以在 Erik Wiffin 恰当命名的网站0.30000000000000004.com上看到以多种不同语言打印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 中的数字你可能不知道的 3 件事

如果你用 Python 写过任何东西,那么你可能在你的一个程序中使用了一个数字。但数字不仅仅是它们的原始值。

在 Python 中比较浮点数的正确方法大卫·阿莫斯大卫·阿莫斯

在 Python 中比较浮点数的正确方法

其他资源

  • 浮点运算:问题和限制
  • 浮点指南
  • 浮点的危险
  • 浮点数学
  • 每个计算机科学家都应该知道的关于浮点运算的知识
  • 如何在 Python 中对数字进行四舍五入

想将您的 Python 技能提升到一个新的水平吗?我为 Python 编程和技术写作提供一对一的私人辅导。单击此处了解更多信息。

来源: https://davidamos.dev/the-right-way-to-compare-floats-in-python/

发表回复 取消回复

要发表评论,您必须先登录。

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme