Python_进阶_浮点型(float)不可避免的误差产生原因,float类型在所有计算机中的储存方式——获得图灵奖的天才设计:IEEE754 标准
目录
说明
本文仅作为学习分享
本文在描述IEEE754 标准时,仅描述了双精度浮点型(64位)的情况。但单精度浮点型与其理论相同,所以不再赘述。
关键词:
IEEE754标准;Float;Python;二进制舍入
一、前言
在学习计算机语言时,经常会遇到一个很经典的BUG——计算机在计算小数时一定会产生误差,特别是在科学计算时。
本科时的老师就让我们有兴趣的话课下可以学习一下这方面的知识,但当时仅仅只是浅显的了解了一下。直到这两天才深入学习了这方面的知识。
本来我以为这是一个很小的知识点,但学习之后才发现这是一个非常重要的系统化的知识。
深入学习非常有助于理解计算机和计算机语言,也会对软件和硬件的关系有更深刻的认识。
IEEE754 标准是获得图灵奖的天才设计,本文将较为详细的描述这个浮点数处理的基石。
二、理论
2.1 Bug
在python3.0中,如果我们直接将两个小数相加,大概率是个错误结果

0.1+0.2的结果明明是0.3,可是计算机却出现了这种显而易见的低级错误
这明显是错误的,可是是什么导致了这个错误呢?
我们大致需要以下知识:
1:十进制小数转换为二进制
2:IEEE754标准
3:二进制舍入方法
2.2 十进制小数转换为二进制小数
十进制小数转换为二进制小数的方法是——整数部分展除二取余并逆位读数,小数部分乘二取整并顺位读数。
这方面知识不会的请去查阅,在此不再赘述。
例如3.5这个小数
计算结果为整数部分11,小数部分1。所以整体为:0b11.1
现在验证一下

结果正确
2.3 IEEE754标准
现在简单介绍一下电气电子工程师学会(IEEE)754浮点数储存标准
IEEE 754是计算机中表示浮点数的通用标准,定义了二进制浮点数的格式、精度、舍入规则及异常处理方式。该标准广泛应用于硬件和软件实现,确保浮点数运算的跨平台一致性。
IEEE754标准可以表达为:(-1)^Sign * Base^Exponent * Mantissa
其中“*”为乘号
其中“^”为指数符号 (在python中为“**”)
其中Sign为正负标志
其中Mantissa为尾数,或者说有效数值,这个数值必须且仅保留一位正数位
其中Base为基数,或者说进制
其中Exponent为指数,或者说小数进位数
此标准适用于所有浮点数,包括软件和硬件
看不懂没关系,下面会举例
2.4 IEEE754标准实际储存的格式
上文的表达式是IEEE754标准的表达式,但和实际储存方式有细微的差别
2.4.1 通用情况
Sign符号位很简单,正数为0,负数为一
Base更简单,这是在确定进制之后的固定值,计算机储存一般为2
Mantissa也很简单,将有效数值小数点后的数值储存就行,因为规格化浮点数的小数正数部分一定为一(规格化浮点数后面解释)
但在储存Exponent时有一个问题,如果指数是负数,那么需要一个额外的符号位去表示指数的正负
在python中,也就是双精度浮点型标准下,实际指数储存时会加上11位指数位最大值的一半,也就是储存的Exponent_input=Exponent+1023
我们将这个指数位最大值的一半1023称之为偏移量
通过加上这个偏移量,可以直接使得储存的所有指数的值皆大于等于零,不需要额外的符号位
那么显而易见的是,理论上11位指数位0-2047可以表示-1023—+1024这2048个指数值
2.4.2 特殊情况
IEEE754标准规定浮点数在储存指数时保留两种特殊情况
1:指数部分全为1时
若尾数部分为零,则此时表示无穷大
且若符号位为1时,则表示负无穷大
且若符号位为0时,则表示正无穷大
若尾数部分不为零,则表示NaN(Not a Number),即无效数字
2:指数部分全为零时,表示这是一个非规格化浮点数
2.4.3 规格化浮点数与非规格化浮点数
我们先来解释什么是规格化浮点数
python在储存尾数时,直接舍弃了整数部分的一位,因为正常情况下浮点数二进制储存的正数位一定是1,所以我们可以节省下这一位的空间
在这种正数位为1的情况下的浮点数,我们称之为规格化浮点数
规格化浮点数在计算原始指数时,只需要将储存值减去偏移量,即Exponent=Exponent_input-1023
但在非规格化浮点数的情况下,也就是指数为零时,尾数舍弃的正数部分变为了0
这时的原始指数Exponent=Exponent_input-1023这个公式不再适用,因为非规格化浮点数的正数部分不合规格,即无有效的正数部分
此时,原始指数Exponent=1-1023,恒等于-1022
此时,又会出现特殊情况,即尾数为0
IEEE754标准规定:当指数为零,尾数为零时,值为0
且当符号为正时,表示0+
且当符号为负时,表示0-
可是,为什么要这么做呢?这不是看起来多此一举吗?
其实答案就在默认隐含掉的正数部分上
如果我们不引入非规格化浮点数,那么就默认尾数的正数部分为1
此时最小数为1.0*2**(-1022)
也就是说此时,我们虽然指数的数量级最小了,但尾数完全不是最小数量级
我们浪费掉了52位尾数位的数量级
但如果引入非规格化浮点数之后,隐含掉的正数部分变为了0
于是此时我们可以完全利用尾数的52位
此时的最小数为2**(-52)*2**(-1022),整整差了52个数量级
通过上述的内容,我们也能得出一个结论,双精度浮点数的精度为2**(-1074)
2.4.4 通用储存格式
经过上文的论述,我们已经对python双精度浮点数的储存模式有了清晰的概念
本文只描述规格化浮点数的情况,但其实非规格化浮点数,0,无穷大和NaN原理相同,读者可以自行推导。
python中以二进制储存(或者说绝大部分的计算机语言都用二进制储存),那么基数是确定的,也就是2。
而python舍弃了整数部分的1位,所以实际的尾数为Mantissa-1,仅保留小数部分
对于规格化浮点数,Exponent_input=Exponent+1023
所以python的浮点数表达式为:(-1)**Sign * Mantissa * (2**Exponent)
实际储存时,从高到低的数据格式为:sign[0]Exponent[10:0]Mantissa[51:0]
实际储存时,从高到低的实际数据为:[sign][Exponent+1023][Mantissa-1]
说起来有点复杂,但其实很简单,现在我举个例子
例如上文中的小数3.5,二进制为:0b11.1
首先它是个正数,那么S为0
有效值的进位为1,所以Exponent为1+1023=1024,1024转换为二进制为100 0000 0000
它的有效数值为1.11,那么Mantissa=1.11,我们只取小数点后的有效数值11
将他们拼接起来,所以小数3.5在python中实际储存为:
0_10000000000_1100000000000000000000000000000000000000000000000000
现在我们验证一下

计算与结果完全一致
2.5 二进制舍入原理
现在我们回到最开始的问题,为什么会出现那个Bug
实际上,除了小数点后最后一位为5的所有小数,在计算机中都并不精准
从我们刚刚学习的浮点数储存方式中,我们知道小数部分是用乘二取整法转换的
那么python中储存0.1的实际数据是什么呢?
理论上,0.1转换为二进制为0.0001100110011....0011
这是一个无限循环
根据上文的推导,可以简单得出sign=0,exponent=-4
故exponent_input=-4+1023=1019,转换为二进制为001111111011
mantissa=1.100110011....0011,舍去正数位为100110011....0011
故整体储存为:0_001111111011_100110011....
但是我们知道,我们只有52位尾数位,所以必然不是精确数值
在python3.0中,float储存遵循的是IEEE754标准中的“最近舍入,平局则偶”
意思是尾数的舍入选取离实际数值最近的那个数值,如果有两个数值离实际数值一样近,则选择偶数数值(十进制下的最后一位)
但是如何在计算机的二进制中实现这个目标呢?
接下来我将介绍另一个天才设计—二进制“最近舍入,平局则偶”舍入方法
这个方法需要三个标志位:
保护位(Guard bit, G):紧跟在有效数字最低位之后的第一位。
舍入位(Round bit, R):保护位之后的第二位。
粘滞位(Sticky bit, S):舍入位之后的所有剩余位经过逻辑或运算得到的一个标志位(只要剩余位中有1,S就为1;全为0则S为0)
通过 G、R、S 三个位的组合,可以准确判断中间结果与两个最近可表示浮点数的距离关系
情况一:G=0
则说明靠近较小的数,向下舍入
情况二:G=1 且 (R或S=1)
则说明靠近较大的数,向上舍入
情况三:G=1 且 R=0 且 S=0
平局
舍入结果:根据目标有效数字的最低位(LSB)决定:
如果 LSB = 0(偶数),则向下舍入(保持最低位为 0);
如果 LSB = 1(奇数),则向上舍入(使最低位变为 0,因为加 1 后最低位变为 0,可能进位到更高位)
我们可以简单得出,0.1的最后一字节数据为1001,后续数据为:1001001001....
故G=1,R=0,S=1
那么根据以上原则,应向上舍入,即将1001舍入为1010
所以,整体数据为:
0_01111111011_1001100110011001100110011001100110011001100110011010
三、计算结果验证
3.1 验证
我们先验证一下0.1的计算推导结果

计算与结果完全相等
经过计算,这个数值实际为
0.1000000000000000055511151231257827021181583404541015625
同理可得,0.3在储存中的实际数值为
0.200000000000000011102230246251565404236316680908203125
在python中将这两个数值相加

与预期一致
3.2 误差消除方法
想要消除这种误差,实际使用python等计算机语言时有很多方法可以做到
这些方法都极其简单,比如decimal模块、fractions模块、使用整数运算(缩放法)、比较时使用容差(math.isclose)、格式化输出时控制小数位数等
在此不再赘述
当然,这也是一个很好的课题
四、整体代码及注释
#接下来我们看一个非常经典的计算机蝽(Bug) float_num_error=0.1+0.2 print(float_num_error) #输出结果为0.30000000000000004 #这明显是错误的,可是是什么导致了这个错误呢? #我们需要以下知识: #1:十进制整数即小数部分转换为二进制 #2:IEEE754双精度浮点数(64位)储存标准 #首先我们先解释十进制小数转换为二进制小数 #十进制小数转换为二进制小数的方法是——整数部分展除二取余并逆位读数,小数部分乘二取整并顺位读数 #这方面知识不会的请去查阅,在此不再赘述 #例如3.5 #计算结果为整数部分11,小数部分1。所以整体为:0b11.1 #现在验证一下 float_num=0b11+0b1/(1<<1)#python无法直接表达二进制小数,故采用组合方式,新手不用管 print(float_num) #结果正确 #现在简单解释一下电气电子工程师学会(IEEE)754双精度浮点数(64位)储存标准 #IEEE754标准可以表达为:(-1)**Sign * Mantissa * Base**Exponent #其中“*”为乘号 #其中“**”为指数符号 #其中Sign为正负标志 #其中Mantissa为尾数,或者说有效数值,这个数值必须且仅保留一位正数位 #其中Base为基数,或者说进制 #其中Exponent为指数,或者说小数进位数 #现在的问题是,python等计算机实际储存的格式 #Sign符号位很简单,正数为0,负数为一 #Base更简单,这是在确定进制之后的固定值,计算机储存一般为2 #Mantissa也很简单,将有效数值小数点后的数值储存就行 #但在储存Exponent时有一个问题,如果指数是负数,那么需要一个额外的符号位去表示指数的正负 #在python中,实际指数储存时会加上11位指数位最大值的一半,也就是储存的Exponent_input=Exponent+1023 #我们将这个指数位最大值的一半1023称之为偏移量 #通过加上这个偏移量,可以直接使得储存的所有指数的值皆大于等于零,不需要额外的符号位 #那么显而易见的是,理论上11位指数位0-2047可以表示-1023—+1024这2048个指数值 #但是IEEE754标准规定浮点数在储存指数时保留两种特殊情况 #1:指数部分全为1时 #若尾数部分为零,则此时表示无穷大 #且若符号位为1时,则表示负无穷大 #且若符号位为0时,则表示正无穷大 #若尾数部分不为零,则表示NaN(Not a Number),即无效数字 #2:指数部分全为零时,表示这是一个非规格化浮点数 #我们先来解释什么是规格化浮点数 #python在储存尾数时,直接舍弃了整数部分的一位,因为正常的浮点数二进制储存的正数位一定是1,所以我们可以节省下这一位的空间 #在这种正数位为1的情况下的浮点数,我们称之为规格化浮点数 #规格化浮点数在计算原始指数时,只需要将储存值减去偏移量,即Exponent=Exponent_input-1023 #但当指数为零时,尾数舍弃的正数部分变为了0 #这时的原始指数Exponent=Exponent_input-1023这个公式不再适用,因为非规格化浮点数的正数部分不合规格,即无有效的正数部分 #此时,原始指数Exponent=1-1023,恒等于-1022 #此时,又会出现特殊情况,即尾数为0 #IEEE754标准规定:当指数为零,尾数为零时,值为0 #且当符号为正时,表示0+ #当符号为负时,表示0- #为什么要引入非规格化浮点数?其实答案就在隐含掉的正数部分上 #如果我们不引入非规格化浮点数,那么就默认尾数的正数部分为1 #此时最小数为1.0*2**(-1022) #也就是说此时,我们虽然指数的数量级最小了,但尾数完全不是最小数量级 #我们浪费掉了52位尾数位的数量级 #但如果引入非规格化浮点数之后,隐含掉的正数部分变为了0 #于是此时我们可以完全利用尾数的52位 #此时的最小数为2**(-52)*2**(-1022) #整整差了52个数量级 #通过上述的内容,我们也能得出一个结论,双精度浮点数的精度为2**(-1074) #经过上文的论述,我们已经对python双精度浮点数的储存模式有了清晰的概念 #python中以二进制储存(或者说绝大部分的计算机语言都用二进制储存),那么基数是确定的,也就是2。 #而python舍弃了整数部分的1位,所以实际的尾数为Mantissa-1 #在此本文只描述规格化浮点数的情况 #对于规格化浮点数,Exponent_input=Exponent+1023 #所以python的浮点数表达式为:(-1)**Sign * Mantissa * (2**Exponent) #实际储存时,从高到低的数据格式为:sign[0]Exponent[10:0]Mantissa[51:0] #实际储存时,从高到低的实际数据为:[sign][Exponent+1023][Mantissa-1] #说起来很复杂,但其实很简单,现在我举个例子 #例如上文中的小数3.5,二进制为:0b11.1 #首先它是个正数,那么S为0 #它的有效数值为1.11,那么Mantissa=1.11,且Mantissa-1=0.11,我们只取小数点后的有效数值11 #它的进位为1,所以Exponent为1+1023=1024,1024转换为二进制为100 0000 0000 #将他们拼接起来,所以小数3.5在python中实际储存为:0_10000000000_1100000000000000000000000000000000000000000000000000 #现在我们验证一下 import struct def float_to_binary64(f): #将浮点数打包为8字节(默认使用本机字节序,通常为小端) bytes_=struct.pack('d',f) #将每个字节转换为8位二进制并拼接并转换为大端序.join(f'{b:08b}' for b in struct.pack('>d', f)) return bits f = 3.5 #将任何你想测试的值填入f bits = float_to_binary64(f) print(bits) #计算与结果完全一致 #现在我们回到最开始的问题,为什么会出现那个Bug #实际上,除了小数点后最后一位为5的所有小数,在计算机中都并不精准 #从我们刚刚学习的浮点数储存方式中,我们知道小数部分是用乘二取整法转换的 #那么python中储存0.1的实际数据是什么呢? #理论上,0.1转换为二进制为0.0001100110011....0011 #这是一个无限循环 #根据上文的推导,可以简单得出sign=0,exponent=-4 #故exponent_input=-4+1023=1019,转换为二进制为001111111011 #mantissa=1.100110011....0011,舍去正数位为100110011....0011 #故整体储存为:0_001111111011_100110011.... #但是我们知道,我们只有52位尾数位,所以必然不是精确数值 #在python3.0中,float储存遵循的是IEEE754标准中的“最近舍入,平局则偶” #意思是尾数的舍入选取离实际数值最近的那个数值,如果有两个数值离实际数值一样近,则选择偶数数值(十进制下的最后一位) #但是如何在计算机的二进制中实现这个目标呢? #接下来我将介绍另一个天才设计—二进制“最近舍入,平局则偶”舍入方法 #这个方法需要三个标志位: #保护位(Guard bit, G):紧跟在有效数字最低位之后的第一位。 #舍入位(Round bit, R):保护位之后的第二位。 #粘滞位(Sticky bit, S):舍入位之后的所有剩余位经过逻辑或运算得到的一个标志位(只要剩余位中有1,S就为1;全为0则S为0)。 #通过 G、R、S 三个位的组合,可以准确判断中间结果与两个最近可表示浮点数的距离关系 #情况一:G=0 #则说明靠近较小的数,向下舍入 #情况二:G=1 且 (R或S=1) #则说明靠近较大的数,向上舍入 #情况三:G=1 且 R=0 且 S=0 #平局 #舍入结果:根据目标有效数字的最低位(LSB)决定: #如果 LSB = 0(偶数),则向下舍入(保持最低位为 0); #如果 LSB = 1(奇数),则向上舍入(使最低位变为 0,因为加 1 后最低位变为 0,可能进位到更高位)。 #我们可以简单得出,0.1的最后一字节数据为1001,后续数据为:1001001001.... #故G=1,R=0,S=1 #那么根据以上原则,应向上舍入,即将1001舍入为1010 #所以,整体数据为——0_01111111011_1001100110011001100110011001100110011001100110011010 #验证 import struct def float_to_binary64(f): #将浮点数打包为8字节(默认使用本机字节序,通常为小端) bytes_=struct.pack('d',f) #将每个字节转换为8位二进制并拼接并转换为大端序.join(f'{b:08b}' for b in struct.pack('>d', f)) return bits f = 0.1 #将任何你想测试的值填入f bits = float_to_binary64(f) print(bits) #计算与结果完全相等 #经过计算,这个数值实际为0.1000000000000000055511151231257827021181583404541015625 #同理可得,0.3在储存中的实际数值为0.200000000000000011102230246251565404236316680908203125 #将这两个数值相加 float_num=0.1000000000000000055511151231257827021181583404541015625+0.200000000000000011102230246251565404236316680908203125 print(float_num) #与预期一致 #想要消除这种误差,有很多方法可以做到 #这些方法都极其简单,比如decimal模块、fractions模块、使用整数运算(缩放法)、比较时使用容差(math.isclose)、格式化输出时控制小数位数等 #在此不再赘述 #但最重要的是,要理解这种误差,并且平衡效能(速度)和误差之间的关系。有的时候,没有必要为了无效的精准度而牺牲效率五、结语
IEEE754标准,这个浮点数的基石,我认为诠释了电子数学的美。
电子数学与数学的区别是,我们不得不面对物理、器件、技术、材料等的限制。电子技术没有最好,只有更好
我们必须在精度、范围和效率之间找到一个平衡点
就像生活一样不是吗?
本躯能力不足,若有错误恳请斧正
五、参考
- C语言中文网 n.d., '小数在内存中是如何存储的,揭秘诺贝尔奖级别的设计(长篇神文)', C语言中文网, viewed 28 February 2026, http://c.biancheng.net/
- ZEEKLOG博客 2024, '控制python中的浮点精度', 腾讯云开发者社区, viewed 28 February 2026, https://cloud.tencent.cn/developer/information/控制python中的浮点精度
- Oracle Corporation 2016, Numerical Computation Guide, part number E71940-01, Oracle Developer Studio 12.5, viewed 28 February 2026, https://docs.oracle.com/cd/E71940_01/html/E71991/z4000ac019878.html
- Stack Overflow 2014, 'Python3 rounding to nearest even', Stack Overflow, viewed 28 February 2026, https://stackoverflow.com/questions/23248489
- 科普中国 2019, '双精度浮点数', 科普中国·科学百科, viewed 28 February 2026, https://cloud.kepuchina.cn/newSearch/imgText?from=1&id=6974140214012293121