14. 浮点算术: 问题和限制¶

14. 浮点算术: 问题和限制¶

14. 浮点算术: 问题和限制¶

浮点数在计算机中以二进制的除法表示. 比如, 十进制的:

0.125

其值为 1/10 + 2/100 + 5/1000, 同样, 以二进制表示则为:

0.001

其值为 0/2 + 0/4 + 1/8. 这两种表示法的值是一样的, 唯一的区别是,

前者以十进制表示, 而后者则以二进制表示.

不幸的是, 大多数的十进制小数都无法严格的以二进制来表示.

一个结果就是, 普遍来说, 你输入的十进制的小数, 通常只是以接近的二进制数表示.

在十进制中这个问题很容易理解. 考虑分数 1/3 . 你可以用一个接近的十进制数表示:

0.3

要更一些,

0.33

更好一些,

0.333

等等. 但是不管你怎么写, 都不是严格的等于 1/3, 但是可以使结果更接近于 1/3.

同样, 无论你用了多少位, 二进制的数也无法精确表示十进制的 0.1 .

在以二为底的情况下, 将是个无限循环

0.0001100110011001100110011001100110011001100110011...

在任意有限的位上停止, 可以得到一个近似值. 在今天的大多数机子上,

浮点数近似地使用二元的分数来表示, 其分子是一个 53 位的数字,

分母则是 2 的幂. 像 1/10, 其二进制表示为 3602879701896397 / 2 ** 55.

这个很接近, 但不是准确的等于.

很多用户因为这些值显示的方法而并不知道近似. Python 仅仅打印一个合适的十进制分数,

而真正的二进制近似还是存储于机器中. 在大多数情况下, 如果让 Python 打印一个十进制小数,

那么其会以真实存储的数字显示, 例如 0.1

>>> 0.1

0.1000000000000000055511151231257827021181583404541015625

此处的位数比一般用到的要多, 所以 Python 使用一个近似的值代替之:

>>> 1 / 10

0.1

只要记住, 尽管打印的值看起来是准确的 1/10 , 但是真正存储的只是最接近的二元分数罢了.

有趣的是, 有很多不同的值共享同样的分数. 举个例子, 数字 0.1 和

0.10000000000000001 及 0.1000000000000000055511151231257827021181583404541015625

都是 3602879701896397 / 2 ** 55 的近似. 因为同样的分数值表示不同值的近似,

这样任何的值都可以保证不变式 eval(repr(x)) == x.

历史原因, Python 的提示符和内置的 repr() 函数会选择 17 位,

0.10000000000000001 . 在 Python 3.1 开始, Python (在绝大数的系统上),

会选择最简的表示 0.1.

注意, 二进制表示的浮点数在此处是非常自然的: 这个不是 Python 中的 bug,

也不是你代码中的 bug . 在很多支持硬件浮点型的语言中也可以看到这样的事情

(尽管有些语言默认下不会显示不同, 或在所有的输出模式下).

对于更多友好的输出, 你可能需要使用字符串格式化来产生一个有限制的数字:

>>> format(math.pi, '.12g') # give 12 significant digits

'3.14159265359'

>>> format(math.pi, '.2f') # give 2 digits after the point

'3.14'

>>> repr(math.pi)

'3.141592653589793'

有一点很重要, 你需要意识到, 在真实情况下, 这是个幻觉:

你仅仅是四舍五入了显示的真实值.

其中一个幻觉会产生另一个. 举个例子, 因为 0.1 并不是严格的 1/10,

三个 0.1 相加并不会生成准确的 0.3:

>>> .1 + .1 + .1 == .3

False

同样, 因为 0.1 不能够得到更接近 1/10 的值, 而 0.3 不能得到更接近 3/10 的值,

因此使用 round() 函数来进行四舍五入也是不起作用的:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)

False

尽管数字不能够更接近它们理想上的准确值, round() 函数在计算后使用,

确实可以实现两个数字之间的比较:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)

True

像这样的浮点数算法, 会导致很多令人奇怪的事情. “0.1” 的问题将在后面更详细的解释,

具体看 “Representation Error” 那节.

更多常见的令人吃惊的事情, 可以参考

The Perils of Floating Point .

就像后面说的, “没有简单的答案.” 但是, 对于浮点数请不要过度谨慎.

在 Python 中这些浮点数的问题来源于其硬件, 在多数的机器中, 浮点的精度没有必要达到

1/2**53 . 对于普通的任务已经足够了, 你需要记住的就是, 这不是算数的问题,

每个浮点数操作都会遇到这样的问题.

当不合理的情况真的存在时, 对于多数情况, 你最终还是能得到希望的结果,

如果你将显示的数值进行四舍五入, 并确保你所需要的位数.

str() 函数经常就能满足需要了, 使用 str.format() 方法,

来指明其 Format String Syntax.

在需要严格的数值表示时, 试试使用 decimal 模块,

这个模块实现了用于账目运算或更高精度时用到的数值算法.

另一种就是 fractions 模块, 它实现了基于有理数的算法

( 所以 1/3 就可以准确的表述 ).

如果你有大量浮点数的运算, 那么你可以看看 Python 的数值库或其他的计算和统计的包,

SciPy 这个项目对此有很好的支持. 参考 .

Python 提供了工具来帮助你获得浮点数的准确值.

你可以使用 float.as_integer_ratio() 方法来表示一个分数:

>>> x = 3.14159

>>> x.as_integer_ratio()

(3537115888337719, 1125899906842624)

因为这个比率是准确的, 它就可以用来比较原始的数字:

>>> x == 3537115888337719 / 1125899906842624

True

float.hex() 方法以十六进制表述,

这也同样给出了一个被你计算机准确存储的值:

>>> x.hex()

'0x1.921f9f01b866ep+1'

前面的十六进制表示, 可以用来重新建立一个浮点值:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')

True

因为这个表示是严格的, 所以对于不同版本的 Python (跨平台) 都是兼容的,

而且也可以和其他的语言进行交换 (比如 java 和 C99).

另一个有用的工具就是 math.fsum() 函数. 它可以在计算总和时减少精度的丢失.

它会记录在求和时丢失的精度. 这样误差就不会积累而最终影响结果了.

>>> sum([0.1] * 10) == 1.0

False

>>> math.fsum([0.1] * 10) == 1.0

True

14.1. 表示错误¶

本节会更详细的解释 “0.1” 的例子, 并且教你如何进行准确的分析.

此处假设你已有了基本的二元浮点数表示的基础.

Representation error 涉及到这样的事实,

有些 (更准确来书是大多数) 小数的分数表示不能够以二进制为底的分数表述.

这就是主要的原因, 为何 Python (或者 Perl, C, C++, Java, Fortran,

还有更多的) 常常不能够如你所愿的表示.

为什么会那样? 1/10 不能够被二进制的分数准确表示.

基本上全部的机器在今 (2000年11月) 来说都是使用了 IEEE-754 浮点数算法,

并且几乎全部的平台将 Python 的浮点映射为 IEEE-754 “double精度”.

754 doubles 包含了 53 位的精度, 所以在计算机中, 0.1 被转成一个很接近的分数,

而它又可以这种 J/2**N 的形式表示, 此处的 J 是一个包含 53 位的整数.

重写:

1 / 10 ~= J / (2**N)

为:

J ~= 2**N / 10

并且记着 J 有严格的 53 位 (也就是 >= 2**52 但 < 2**53),

对于 N 最好的值就是 56:

>>> 2**52 <= 2**56 // 10 < 2**53

True

也就是说, 56 是唯一能让 J 为 53 位的 N 值.

而 J 最有可能的值就是那个商:

>>> q, r = divmod(2**56, 10)

>>> r

6

因为剩余的如果大于10的一半, 那么最好的近似就是进一位:

>>> q+1

7205759403792794

所以以 754 double 精度表示的 1/10 最合适的值就是:

7205759403792794 / 2 ** 56

将分子分母约化:

3602879701896397 / 2 ** 55

注意, 因为我们进了一位, 所以值会比 1/10 稍微大一点;

如果我们没有进位, 那么商又会比 1/10 稍微小点. 但无论如何, 都不是准确的 1/10!

所以计算机从没有看过 1/10: 它看到的是前面给出的分数,

以 754 双进度近似的结果:

>>> 0.1 * 2 ** 55

3602879701896397.0

如果我们将这个分数乘以 10**55, 我们可以看到55位的数字:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55

1000000000000000055511151231257827021181583404541015625

这意味存于计算机中的准确值等于

0.1000000000000000055511151231257827021181583404541015625.

很多语言 (包括 Python 的旧版本) 不是直接显示所有的位数, 而是将其保留为17位有效数字:

>>> format(0.1, '.17f')

'0.10000000000000001'

fractions 和 decimal 模块使这些计算变得简单:

>>> from decimal import Decimal

>>> from fractions import Fraction

>>> Fraction.from_float(0.1)

Fraction(3602879701896397, 36028797018963968)

>>> (0.1).as_integer_ratio()

(3602879701896397, 36028797018963968)

>>> Decimal.from_float(0.1)

Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> format(Decimal.from_float(0.1), '.17')

'0.10000000000000001'

See also

(^.^)

原文: http://docs.python.org/py3k/tutorial/floatingpoint.html

初译: 刘鑫

精译: DocsPy3zh

校对: Zoom.Quiet

复审:

你可能也喜欢

艾菈·冯·杜勒
365bet官网平台网址

艾菈·冯·杜勒

📅 06-22 👀 3397
wireshak常见问题
怎样获得免费office365

wireshak常见问题

📅 11-18 👀 9962
东边敲锣西边响
怎样获得免费office365

东边敲锣西边响

📅 08-26 👀 1618
碧蓝航线光辉怎么得 光辉获得方法一览
怎样获得免费office365

碧蓝航线光辉怎么得 光辉获得方法一览

📅 01-18 👀 4716
红包app排行榜
beat365手机版官方网站正规

红包app排行榜

📅 07-02 👀 1766
开服选哪个区DNF?攻略解析帮你轻松入门
beat365手机版官方网站正规

开服选哪个区DNF?攻略解析帮你轻松入门

📅 01-10 👀 1400