浮点数运算的一些问题
引入
先来看个代码:
print(1-0.7 == 0.3)
很多人会觉得这一看不就是True吗,但实际上结果为False。因为1-0.7的结果为0.30000000000000004
浮点数转二进制的方法
可以用这个网站验证答案:https://c.runoob.com/front-end/58/
因为所有的数据本质都是通过二进制数存储的,所以要分析这个问题的本质,我们得先去看浮点数的二进制表示。因为整数和小数的转换方法不同,所以先将十进制数的整数部分和小数部分分别转换后,再加以合并。
(1)整数部分:整数部分比较简单,因为一个有限的十进制整数都能转换成有限的二进制数。采用除2取余,逆序连接的方法,代码如下:
1 2 3 4 5 6 7 | n = 173 ls = [] while n ! = 0 : ls.append( str (n % 2 )) n / / = 2 ls.reverse() print (''.join(ls)) |
(2)小数部分:小数部分会麻烦一点了,具体的方法为:
''' 如:0.625=(0.101)bin 0.625*2=1.25======取出整数部分1 0.25*2=0.5========取出整数部分0 0.5*2=1==========取出整数部分1 直到积中的小数部分为零为止,从上倒下连接 ''' import math n = 0.625 s = '' while int(n)!= n: n*=2 s+=str(int(n)) n = math.modf(n)[0] print(s)
在这里可以发现,十进制的小数转二进制的时候,很容易就会无限循环下去,例如0.2转换成二进制就i是0011001100110011001100……,由于计算机存储的位是有限的,所以必然会造成精度的损失。
IEEE 754
这里就不得不提到IEEE 754 了,这东西其实就是一个浮点数的运算标准。因为对于有符号整数来说,存储的方式其实很简单,一位表示符号,剩下的位都可以用来表示数值,但浮点数的情况比较复杂。对于float32,也就是32位的浮点数,是这样表示的:V = (-1)^S*M*2^E
(1)(-1)^s 表示符号位,当 s=0,V 为正数;当 s=1,V 为负数
(2)M称为尾数,1≤ M <2,也就是说,M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。
(3)E表示阶码。例如浮点数5.0,用二进制表示是101.0,用科学计数法表示为1.01*2^2(注意,因为这里是二进制数,所以是乘2的2次方而不是10的二次方)。此时阶码E就是2,尾数M为1.01
对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。
尾数
因为M 总是写成 1.xxxxxx *2^E的形式,所以在计算机内部保存 M 时,默认这个数的第一位总是 1,因此可以被舍去,只保存后面的 xxxxxx 部分。比如保存 1.01 的时候,只保存小数部分的 01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省 1 位有效数字。以32位浮点数为例,留给 M 只有 23 位,将第一位的1舍去以后,等于可以保存 24 位有效数字。
阶码
上面说到了,阶码是一个八位的二进制数,并且它是一个无符号数,所以取值范围是[0,255]。但是科学计数法中的指数并不一定都是正数,例如十进制0.5转换为二进制是0.1,因为1<=M<2,所以要写成1*2^-1,所以阶码部分,采用移码来表示。简单地讲,就是阶码的真实值为计算机存储值减去中间数127,比如,2^10 的 E 是 10,所以保存成 32 位浮点数时,必须保存成 10+127=137,即 10001001,这样阶码的范围就变成了[-127,128]。阶码这里又有一些特殊情况:
(1)阶码E全为0,有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx 的小数。这样做是为了表示机器零(计算机中小到机器数的精度达不到的数均视为“机器零”),以及接近于 0 的很小的数字。
(2)阶码E全为1,阶码全为1的情况有两种,用来表示特殊值,第一种是尾数M也全为1,这时表示正负无穷大(正还是负取决于符号位)。若M不全为1,则表示NaN(Not a Number,非数,这个概念也是IEEE 754定义的),例如0除以0的结果。
1-0.7==0.3为什么为False
上面我们解释了为什么1-0.7的结果是0.30000000000000004了,由于计算机存储浮点数的位数有限,所以浮点数转成二进制表示的时候,必然会造成精度的损失,例如0.3转换为二进制表示为:
0.0100110011001100110011001100110011001100110011001101(舍入后的的结果),而把这一串再转换为十进制就是0.30000000000000004
而我们自定义的0.3,虽然本质上也是二进制存储的,它实际上也是0.30000000000000004,但是为了保证浮点数运算运行的结果大部分时候与用户预期一致,所以程序在以十进制表示这个数据的时候,会按照用户自定义的精度对数据进行舍入。例如我们定义a=0.3,那么在对变量a取值的时候,程序就会把0.30000000000000004舍入为0.3,保持输入与输出一致。但是在进行1-0.7计算的时候,因为用户并没有指定期望的结果精度,所以对于运算结果,程序不会做限制,直接取得0.30000000000000004。
遗留的问题
使用加减法很容易出问题,但乘除法不会。
print(1-0.9)#0.09999999999999998 print(0.2/2)#0.1
至于是为什么,暂时还搞不清楚,以后来填坑
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!