Python 精度问题

前奏:

在一个群里看见一段程序

a =10.0

b=10.0

id(a),id(b)表示的内存地址不一样



结果:


精度疑问

在Python命令行中输入:

>>> 0.1
0.10000000000000001
>>> 2.2
2.2000000000000002
>>> 3.3
3.2999999999999998
>>> 3.5
3.5
>>> 1.2
1.2

可以看到有些浮点数是精确的,而有些则是不精确的,和真实值之间会有一个很小的误差。

二进制小数

Python中的浮点数是所谓的双精度浮点数,它采用64个比特保存一个浮点数。

http://en.wikipedia.org/wiki/Double_precision

维基百科中关于双精度浮点数的说明

计算机中的数都是以二进制的形式储存,浮点数也不例外。二进制的小数转换为十进制的小数的方法和整数类似,二进制小数点后的每一位对应的值为
,其中n为小数点后的位数。因此10.1011(2)对应的值为:

因为
是有限小数,因此任意有限的二进制小数转换为十进制都是有限位的。然而反之则不尽然。下让让我们用程序将十进制小数转换为二进制小数。

十进制小数转换为二进制小数

为了将十进制小数转换为二进制小数,我们需要一个能精确表示十进制小数的对象。可以使用Python标准库中decimal模块的Decimal对象实现。

from decimal import Decimal
def binary_float(x, n=100):
    a = Decimal(x)*2
    r = []
    for i in xrange(n):
        if a >= 1:
            r.append("1")
            a -= 1
        else:
            r.append("0")
        a = a*2
    return "0." + "".join(r)

我们用Decimal对象表示十进制小数,这样不会产生任何误差。然后循环将此对象乘以2,若结果大于1,则输出1并减去1,否则输出0。通过这种方法可以产生二进制小数上的各个位。

下面用binary_float()输出0.1的二进制小数形式,由于浮点数字面量已经不精确,因此需要用字符串表示十进制浮点数:

>>> binary_float("0.1")
0.00011001100110011001100110011001100110011001100110011001100...
寻找循环节

从上面的结果可以看出0.1对应的二进制小数是一个无限循环小数。我们可以用下面的程序找到二进制小数的循环节:

def binary_float2(x, n=100):
    a = Decimal(x)*2
    r = []
    visited_set = set()
    visited_list = []
    for i in xrange(n):
        if a in visited_set: ❶
            break
        visited_set.add(a)
        visited_list.append(a)
        if a >= 1:
            r.append("1")
            a -= 1
        else:
            r.append("0")
        a = a*2
    r.insert(visited_list.index(a), "[") ❷
    return "0." + "".join(r) + "]"

程序中通过visited_set集合和visited_list列表保存已经每次运算之后的Decimal对象。如果某个数值重复出现,那么就找到了循环节。❶visited_set是一个集合,因此用来快速判断值是否重复。❷而数值重复的位置,即循环节开始的位置则需要从visited_list列表中去寻找。

>>> binary_float2("0.1")
'0.0[0011]'
>>> binary_float2("0.81")
'0.11[00111101011100001010]'
浮点数不精确的原因

通过上面的分析可以看出,许多十进制小数转换成二进制小数变成无限循环小数。而在Python中,浮点数是用64个比特保存的,因此会截去超出的部分,从而造成误差。通过浮点数对象的hex()方法可以查看其二进制形式,例如:

>>> (2.6875).hex()
'0x1.5800000000000p+1'

其中,“1.5800000000000”中每个数字都是一个16进制的数,我们把它重写为二进制得到:1.010110000000...,“p+1”部分类似于十进制的科学计数法,它表示小数点要向右移动一位,因此2.6875是使用二进制小数10.1011(2)表示的,这个值是完全精确的。下面再看0.1对应的二进制数:

>>> (0.1).hex()
'0x1.999999999999ap-4'

把它展开为二进制小数,其结果为:

0.00011001100110011001100110011001100110011001100110011010         内存中所保存的0.1对应的二进制小数

而0.1对应的真正的二进制小数为:

0.0001100110011001100110011001100110011001100110011001100110011... 0.1对应的真正的二进制小数

与真正的二进制小数相比,显然内存中所保存的稍大一些,二者之间相差了约:

0.0000000000000000000000000000000000000000000000000000000001101

即在小数点之后第58和59位有一个1,它对应的值为
,约等于5.2e-18,四舍五入到第17位上为1。因此会有:

>>> 0.1
0.10000000000000001

使用SymPy的N(),可以查看浮点数所对应的实际值,下面分别查看0.1和0.12到小数点后30位:

>>> from sympy import N
>>> N(0.1, 30)
0.100000000000000005551115123126
>>> N(0.12, 30)
0.119999999999999995559107901499
>>> 0.12
0.12

我们看到0.12其实也不精确,但是保留小数点后17位时正好四舍五入为0.12了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值