Python3.7官方向导翻译之Python浮点数运算

计算机硬件中将浮点数表示为基2(二进制)分数。 例如,十进制小数部分0.125具有1/10 + 2/100 + 5/1000的值。同样,二进制小数 0.001具有值0/2 + 0/4 + 1/8。 这两个小数具有“相同的值”,唯一真正的区别是第一个分数用10进制表示,第二个用2进制表示。

不幸的是,大多数十进制小数不能完全被二进制小数来表示。 结果是,一般来说,输入的十进制浮点数只能用实际存储在机器中的二进制浮点数来近似。

首先十进制的问题更容易理解。考虑十进制的1/3。 您可以将其近似为小数:0.3,或者好一点,0.33,再好一点0.333等。无论你在后面写多少个数字,
结果永远不会是精确的1/3,但是会无限接近1/3。

同样,无论你用多少个2进制数,值0.1也不能精确的用二进制分数来表示。基于2进制,1/10是一个无限重复的小数:0.0001100110011001100110011001100110011001100110011…

停在任何有限数量的位上,然后你能获得一个近似值。 在今天的大多数机器上,浮点数使用二进制分数来近似,分子使用以最高有效位开始的前53位,并以分母作为2的幂。 以1/10为例,二进制分数是3602879701896397/2 ** 55,它接近但不完全等于1/10的真实值。

由于值的显示方式,许多用户并未意识到这是近似值。 Python仅输出一个近似的十进制值,这个值是以二进制近似存储在机器上的。 在大多数机器上,如果Python打印0.1的二进制近似值的真正十进制值,则会显示0.100000000000000055511151231257827021181583404541015625

这比大多数人认为有用的数字都要多,因此Python通过显示舍入值来保持可管理的数字位数

请记住,即使打印结果看起来像是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中的错误,也不是代码中的错误。 在所有语言中,您都会看到支持硬件浮点运算的相同类型的东西(尽管某些语言默认情况下可能不显示差异,或者在所有输出模式下都不显示差异)。

为了获得更愉悦的输出,你可能希望使用字符串格式来产生有限数量的有效数字:

import math
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
0.30000000000000004

另外,由于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

就像接近尾声说的那样,“没有简单的答案。”但是,不要过分警惕浮点! Python浮点操作中的错误是从浮点硬件继承而来的,并且在大多数机器上,每次操作的2 ** 53次数不超过1个部分。 这对于大多数任务来说已经足够了,但是您需要记住它不是十进制算术,并且每个浮点操作都会遭受新的舍入错误。

虽然存在病态情况,但对于大多数使用的浮点算法,如果只是将最终结果显示为所期望的小数位数,则最终会看到预期的结果。 str()通常就足够了,为了更好的控制,请参阅Format String Syntax中的str.format()方法的格式说明符。

对于需要精确十进制表示的用例,请尝试使用实现十进制算术的十进制模块,该模块适用于会计应用程序和高精度应用程序。

精确算术的另一种形式由fraction模块支持,该分数模块基于有理数进行算术运算(因此像1/3那样的数字可以精确地表示)。

如果您是浮点运算的重度用户,那么您应该查看一下Numerical Python包以及由SciPy项目提供的用于数学和统计操作的许多其他包。 参见https://scipy.org

Python提供的工具可以帮助您在罕见的情况下确实知道浮点数的确切值。 float.as_integer_ratio()方法将float的值表示为一个分数:

x = 3.14159
x.as_integer_ratio()
(3537115888337719, 1125899906842624)

由于该比率是准确的,因此可用于无损地重新创建原始值:

x == 3537115888337719 / 1125899906842624
True

float.hex()方法以十六进制表示浮点数(基数为16),同样给出计算机存储的确切值:

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

表示错误

本节详细介绍“0.1”示例,并说明如何自己对这种情况进行精确分析。 假定对二进制浮点表示基本熟悉。

表示错误是指一些(大部分,实际上)小数部分不能完全表示为二进制(基2)分数。 这是Python(或Perl,C,C ++,Java,Fortran等等)经常不会显示您期望的确切十进制数的主要原因。

这是为什么? 1/10不能完全表示为二进制分数。 今天(2000年11月)几乎所有机器都使用IEEE-754浮点算法,几乎所有平台都将Python浮点映射到IEEE-754“双精度”。 754双精度包含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是N的唯一值,它使得J恰好有53位。 J的最佳可能值是商数四舍五入:

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

由于余数超过10的一半,所以通过四舍五入得到最佳近似值:

q+1
7205759403792794

因此754双精度中1/10的最佳近似值为:7205759403792794 / 2 ** 56

将分子和分母都除以2将分数减小为: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'

fraction和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'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值