一个有趣的问题
假如你尝试进行这个计算:
>>> 0.3 == 0.1 + 0.2
False
有趣的是,结果居然是False。
那么0.3 为什么不等于 0.1 + 0.2 ?
Python官方的解答
Python官网教程里特地提到了这个问题。有个例子解释得特别好,官方是这么说的,我简单概括一下:
你无法将 1/3
(三分之一)表示成一个精确的小数。因为它是除不尽的,只能用0.3,0.33, 0.333……来近似表示它。并且,你的笔墨和纸张有限,只能将它写到0.33333333333,通过四舍五入,近似的表示它。这样一来,可以清楚地明白,0.33333333333并不等于 1/3
(三分之一)。
浮点数也是同理,由于计算机内部是以二进制的方式存储数据的,所以一个小数会被转换为以2进制表示的值,这一转换过程经常“除不尽”造成了舍入的问题。
在MySQL官方文档中将浮点数备注为“大约值”(Approximate Value)。这个描述是比较形象的。0.3 是近似的,0.1 和 0.2 也是近似的表示,所以两者并不相等。
不过正如官方文档所说,不必过于担心浮点数的问题!
在大多数生产生活场景中,你只需将它舍入到所期望的小数位数即可。就像我们在衡量身高的时候,一般只需探讨多少厘米,没有人会在意谁比谁高0.00000001厘米!
但在需要精确精算的场合,例如科学研究、会计金融等领域,请使用decimal模块表示精确的小数,用fractions模块表示精确的分数。这两个模块表示的数字是完全精确的,符合数学上的期望。
Python浮点数到底是什么
Python的官方实现来自cpython,也就是用C语言编写的Python。cpython的源码中,对Python里面的float类型的实现,其实就是一个PyObject_HEAD头加一个double变量。
也即Python浮点数的底层就是double。
cpython源码中的floatobject.h
:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
追根溯源,C语言的double特性又是哪里来的?
C语言的double遵循了IEEE754协议。
IEEE是(国际)电气和电子工程师协会的简称,其下的标准制定机构在信息、通信、电力等多个领域制定了900多个现行的工业标准。(数据来源于wikipedia)
IEEE754是在1985年制定的有关浮点数的标准。
何为“浮点”(Floating Point)
“浮点数”就是小数点会“动”的小数。这其实是采用科学计数法,将小数点的位置标记在指数位上,采用科学计数法下,例如:
- 1000 =
1 * 10^3
(1乘10的3次方) - 0.01 =
1 * 10^-2
(1乘10的-2次方)
这里的3和-2其实就是在说明小数点的位置。
顺便一提,Python字面量直接支持科学计数法,类型是float。可以直接输入形如 2.3e-2
等价于 2.3 * 10^-2
(2.3乘10的-2次方)。这里的e代表以10为基础。
x = 1e2
y = 2.3e-2
# x: 100.0
# y: 0.023
但在计算机里就是以二进制为基础了,必须将10进制转换为2进制的形式,即,一个数乘以2的多少次方的形式,并且分正负数。在形如“正负A乘以2的B次方”中:
- 正负数:用一个符号位(sign bit)来表示 正负1
- A:被称为分数值(fraction),也就是有效数(significant),之与科学计数法的尾数部分有所区别,因为它隐含了高位1
- B:指数偏移值(exponent bias),也就是指数部分
也就是:F = S * F * 2^E
,
也就是:浮点数 = “符号位” 乘以 “有效数” 乘以 2 的 “指数偏移” 次方
,
IEEE754主要制定了两个浮点数标准,单精度(float)和双精度(double)。Python里面的float类型是使用双精度double的。
单精度占4字节,也就是32位,其中包括:符号位1位,指数偏移8位,有效数23位。加上隐含的高位1,十进制的精度可以达到 log 2^24 大约 7.2,也就是7个十进制有效数字。
双精度占8字节,也就是64位,其中包括:符号位1位,指数偏移11位,有效数52位。加上隐含的高位1,十进制的精度可以达到 log 2^53 大约 15.9,也就是15个十进制有效数字。
也就是说,日常使用的话,在15个十进制有效数字范围内使用浮点数是安全的。(这里可以简单联想记忆一下,浮点数的小数点会动嘛,把它想象成小偷,躲得过初一,躲不过十五)
舍入 round
内置函数round用于返回“四舍五入”后的值,它有两个参数,第一个是要进行操作的数字number,第二个是要进行舍入的小数位数ndigits,默认为None,其实就是默认舍入到整数。
例子:
print(round(1.7))
print(round(3.1415, 2))
# 结果:
# 2
# 3.14
round函数有两个需要注意的地方:
其一,对于浮点数的舍入是在二进制下进行的,它按照2进制的标准来舍入,所以在十进制下并不总是表现为“四舍五入”,尤其是处于5这个中间值的时候,可能有差异,例如:print(round(2.675, 2)) 会得到 2.67 而不是 2.68。
其二,如果不是舍入到整数,round对浮点数舍入的结果还是浮点数!这就可以回到最初的那个问题,我用round舍入可不可以?
回到 0.3 == 0.1 + 0.2
用round舍入可不可以?
>>> round(0.3, 1) == round(0.1, 1) + round(0.2, 1)
False
结果仍然是False!
因为round对浮点数舍入的结果还是浮点数,既然都是浮点数,那么又会存在浮点数近似的问题,所以二者并不相等。
如果说这是“事先舍入”的话,你可以尝试“事后舍入”:
>>> round(0.3, 1) == round(0.1 + 0.2, 1)
True
结果为True。
理论上讲,把它拉长到双精度浮点数的有效位数15位也是可以的
>>> round(0.3, 15) == round(0.1 + 0.2, 15)
True
可是这种做法会不会存在什么问题?
如果我面对的是等式两边有可能超过15位有效数字,尤其是整数部分特别长的时候,
123456678991.1296348 == 123456678991.12 + 0.0096348
>>> round(123456678991.1296348, 15) == round(123456678991.12 + 0.0096348, 15)
False
或者浮点数的特殊值(NaN,正负无穷等)的时候,怎么处理?
这就要拿出 epsilon 了。
epsilon
既然都通过“四舍五入”来忽略误差了,这里有更好的忽略误差的方式,就是将两个数做减法,让减法产生的差异小于一个很小的值,也就是可以接受误差的值,这个值就是 epsilon。
要判断 0.3 == 0.1 + 0.2 这个问题,可以改写成:
>>> abs(0.3 - (0.1 + 0.2)) < 0.09
True
相比于 round(0.3, 1) == round(0.1 + 0.2, 1)
这个表达式,这样分析:
如果0.3是“四舍五入”来的,那么它一定在0.25到0.34之间;如果0.1 + 0.2是“四舍五入”来的,那么它一定在0.25到0.34之间。它们之间的差距一定在0.09以下。
实际上,考虑到双精度是15个十进制有效数字,那么可以直接把epsilon的值改到很小:
>>> abs(0.3 - (0.1 + 0.2)) < 1e-15
True
注意到15个有效数字并非小数点后面的数字,所以1e-15并不总是有效,它还取决于前面整数部分的数字有几个,比如当数字特别大的时候,就像之前提到的 123456678991.12,一共14个有效数字,但小数位数只有2位。
所以epsilon的大小应当根据你的目标和实际情况来设置。
将这一判断改写成函数形式,并处理NaN,大致是这样:
def float_equal(a, b, epsilon=0.000001):
if a == b: # 处理 NaN情况
return True
return abs(a - b) < epsilon
其中 epsilon 需要根据实际情况取一个相对合理的值。
总结一下
浮点数是大约值,往往没有准确的值。尤其是,一个特别大的数和一个特别小的数做四则运算时,产生非常大的精度损失。
如果需要判断两个浮点数相等(不是绝对相等,而是相对的,在我们的目标范围内相等),最好是设置一个目标最小值epsilon,将两者做差并取绝对值,让它小于epsilon。
需要精确计算的情形,请使用decimal模块表示小数和fraction模块表示分数。
注意:
虽然本文探讨了可以使用epsilon等方法判断近似相等的情形,但是,一旦有“比较两个数是否相等”这个需求,就尽量不要用float浮点数了,而是使用decimal和fraction来实现。
在这种情况下,强行使用float只会让代码的可读性变得很糟糕,甚至产生很多莫名其妙的问题。
所以,涉及到相等的比较,有精确计算的需求,请使用decimal模块或者fraction模块,而不要用float。