Chinaunix首页 | 论坛 | 博客
  • 博客访问: 957946
  • 博文数量: 33
  • 博客积分: 803
  • 博客等级: 军士长
  • 技术积分: 1755
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-05 18:58
个人简介

《Python科学计算》的作者

文章分类

全部博文(33)

文章存档

2016年(1)

2014年(2)

2013年(3)

2012年(27)

分类: Python/Ruby

2012-02-21 19:59:38

精度疑问

在Python命令行中输入:

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

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

二进制小数

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

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

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

1 .cdot 2^{1} + 0 .cdot 2^{0} + 1 .cdot 2^{-1} + 0 .cdot 2^{-2} + 1 .cdot 2^{-3} + 1 .cdot 2^{-4}

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

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

为了将十进制小数转换为二进制小数,我们需要一个能精确表示十进制小数的对象。可以使用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,它对应的值为2^{-58}+2^{-59},约等于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了。

阅读(3803) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~