前奏:
在一个群里看见一段程序
a =10.0
b=10.0
id(a),id(b)表示的内存地址不一样
结果:
在Python命令行中输入:
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对象实现。
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的二进制小数形式,由于浮点数字面量已经不精确,因此需要用字符串表示十进制浮点数:
0.00011001100110011001100110011001100110011001100110011001100...
从上面的结果可以看出0.1对应的二进制小数是一个无限循环小数。我们可以用下面的程序找到二进制小数的循环节:
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列表中去寻找。
'0.0[0011]'
>>> binary_float2("0.81")
'0.11[00111101011100001010]'
通过上面的分析可以看出,许多十进制小数转换成二进制小数变成无限循环小数。而在Python中,浮点数是用64个比特保存的,因此会截去超出的部分,从而造成误差。通过浮点数对象的hex()方法可以查看其二进制形式,例如:
'0x1.5800000000000p+1'
其中,“1.5800000000000”中每个数字都是一个16进制的数,我们把它重写为二进制得到:1.010110000000...,“p+1”部分类似于十进制的科学计数法,它表示小数点要向右移动一位,因此2.6875是使用二进制小数10.1011(2)表示的,这个值是完全精确的。下面再看0.1对应的二进制数:
'0x1.999999999999ap-4'
把它展开为二进制小数,其结果为:
而0.1对应的真正的二进制小数为:
与真正的二进制小数相比,显然内存中所保存的稍大一些,二者之间相差了约:
即在小数点之后第58和59位有一个1,它对应的值为
,约等于5.2e-18,四舍五入到第17位上为1。因此会有:
0.10000000000000001
使用SymPy的N(),可以查看浮点数所对应的实际值,下面分别查看0.1和0.12到小数点后30位:
>>> N(0.1, 30)
0.100000000000000005551115123126
>>> N(0.12, 30)
0.119999999999999995559107901499
>>> 0.12
0.12
我们看到0.12其实也不精确,但是保留小数点后17位时正好四舍五入为0.12了。