发现问题
首先我来简单说一下我是怎么发现这个问题的。事实上,我有 100 种方法发现这个问题,而你却无能为力~!下面我来列举一种比较简单的方法。学过 Python 的都知道运算符(//)表示整除,运算符(%)表示求余,整除和求余同样也可以用于浮点数,逻辑和两个整数整除和求余一样。然而,在两个浮点数进行求余和整除的过程中可能出现意外,下面来看例子。
>>> 1.0 // 0.2
4.0
>>> 1.0 % 0.2
0.19999999999999996
根据整除和求余的逻辑,显然可以得出浮点数在计算机中的表示存在些许误差。然而,这里有两个浮点数——1.0 和 0.2,因此有三种可能:
- 浮点数 1.0 在计算机中的表示存在误差。
- 浮点数 0.2 在计算机中的表示存在误差。
- 浮点数 1.0 和浮点数 0.2 在计算机中的表示都存在误差。
分析问题
接下来我们尝试分析一下为什么浮点数在计算机中的表示会有这样的误差,在开始进行分析之前,我们首先必须知道,我们都知道,任何数据在计算机中的表示都是二进制的格式。因此一个浮点数要想存入计算机中首先必须得转换成二进制的格式。其中浮点数转换成二进制需要分开来看,小数点左边是整数部分,运用除基取余法,小数点右边是小数部分,运用乘基取整法。浮点数 1.0 运用这种方法,可以轻而易举地得出其对应的二进制表示。因此,我们可以得出只有一种可能是正确的,那就是:浮点数 0.2 在计算机中的表示会存在误差。我敢保证有些人不相信,就立刻马上去尝试使用乘基取整法,发现忙活了半天还是没有得出其对应的二进制的表示,于是就放弃了,并从感觉上认为浮点数 0.2 表示成对应的二进制数会产生误差,这种认识只不过是停留在感性认识,并没有上升到理性认识。毕竟还是存在一些性格顽固的人,他们秉持着不达目的誓不罢休的信条,会一直不停的尝试下去。不要着急,接下来我就会让那些性格顽固的人停下来。在此之前,我先说一下二进制怎么转换成十进制,其过程以及一个结论如下图所示。
下面我们就来证明一下浮点数 0.2 不能被表示成二进制数,既然刚才和 0.2 硬碰使用乘基取整法会出现根本停不下来的诡异的情况,那么我们就不采用这种方法,而是反过来,这里的反过来,有两个方面,首先给出第一个方面:我们选择从二进制转换成十进制入手。有些人可能会奇怪,我连其对应的二进制的表示都计算不出来,该怎么办?!其实它确实是计算不出来,这个时候显然应该使用反证法,假设存在一个二进制小数,使其十进制正好可以等于浮点数 0.2。然后把等式两边变来变去,最后得出等式不成立,矛盾,原假设错误。证明过程如下图所示。
有些人可能会问,这怎么就矛盾了?!等式的左边 2 的 bn 次方是一个偶数。等式右边 5 是一个奇数,2 的 bn-bi (其中 i∈{1, 2, ..., n-1})次方是偶数,其和也为偶数,再加上 1,就变成了奇数,因此等式右边可以看成奇数×奇数,所以等式右边为奇数。等式左边是偶数,右边是奇数,这怎么可能不矛盾?!如果你看懂了我的证明,你还会去尝试使用乘基取整法进行的根本停不下来吗?!既然理论上都无法用二进制精确表示浮点数 0.2,那么在实际中也就更加不可能了,所以上面的例子存在误差已经是板上钉钉的事实了。
接下来我们还可以得出以下两个结论:
- 不允许出现误差的场合(比如表示钱)不能使用浮点数。
- 并不是所有的十进制浮点数(哪怕位数有限),都能用二进制数精确表示。
解决问题
接下来我就简单说一下怎么解决这样的问题。解决这个问题很简单,使用模块 decimal 就足够了,至于这个模块的讲解需要过一段时间。考虑到这次的重点不是模块 decimal,而是上面的三个结论和一个证明。
最后说一些事情,关于 B 站录制的演示视频,我选择的书是《Python 基础教程》(第 3 版)(当然有的时候会讲解一些书上没有的但是很重要的东西)。但是进行讲解我可以懂,你们能不能懂我就不是很清楚了,因此我有打算进行直播答疑,但是我觉得还是要征求大家的意见,下面就来投个票吧?!
下面给出 B 站账号:新时代的运筹帷幄,喜欢的可以关注一下,看完视频不要忘记一键三连啊!