社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

0.1加0.2,为什么不等于0.3?——一节让全班炸锅的Python课

脚本之家 • 5 天前 • 59 次点击  
将 脚本之家 设为“星标⭐”
第一时间收到文章更新
作者 | 张一夫 苏嘉健 
出品 | 脚本之家(ID:jb51net)

那天上 Python的第三节课,讲的是数据类型。为了活跃气氛,我让学生们在交互式命令行里随便算点东西。

没过两分钟,后排一个男生举手了,语气里带着一种发现了新大陆的兴奋:“老师,Python算错了!”

我走过去,他的屏幕上是这样一行:

>>> 0.1 + 0.20.30000000000000004

全班一下子炸了锅。有人不信,自己敲了一遍,结果一模一样;有人开始怀疑人生:“连电脑都不会算小学数学?”还有人当场下结论:“Python有bug,我们换C语言吧。”

我笑着让大家安静下来。我说,这不是Python的bug,换成C、Java、JavaScript,结果都差不多。这节课,我们就来当一回侦探,看看这个多出来的0.00000000000000004,到底是从哪儿冒出来的。

01:先问一个问题:1/3怎么写成小数?

我在黑板上写了一个分数:1/3。

“谁能把它写成小数?”

“0.333333……”

“写完了吗?”

“写不完,无限循环。”

“如果只给你8位,你会写成多少?”

“0.33333333。”

“那三个0.33333333加起来,等于1吗?”

学生们算了算:0.99999999。差了一点点。

我说,好,记住这个感觉。在十进制里,1/3是一个“写不完”的数,只能截断,截断就有误差。电脑遇到的,是同一个问题,只不过它用的是二进制。

02:0.1在二进制里,也是“写不完”的数?

十进制里,只有分母是2和5的组合的分数,才能写成有限小数,比如1/2=0.5,1/4=0.25,1/5=0.2。

二进制里,规则更严格:只有分母是2的幂的分数,才能写成有限小数,比如1/2、1/4、1/8。

那0.1呢?0.1=1/10,分母里有个5,在二进制里就是一个无限循环小数:

0.0001100110011001100110011……

同样,0.2也是。

电脑存储小数,普遍采用一个叫IEEE 754的标准。Python里的float是“双精度”,总共64位,其中真正用来存有效数字的只有52位(加上隐含的一位,相当于53位)。无限循环的二进制小数,到了第53位就得“四舍五入”截断。

于是,电脑里存的0.1,其实不是0.1,而是一个非常非常接近0.1的数。

我让学生们用下面这行代码,把它的“真面目”打印出来:

>>> from decimal import Decimal>>> Decimal(0.1)Decimal('0.1000000000000000055511151231257827021181583404541015625')

屏幕上跳出一长串数字,教室里一片“哇”。原来,我们平时看到的0.1,只是Python在显示时帮我们“美化”过的结果。

两个都带着一点误差的数加在一起,误差也加在一起,结果就成了0.30000000000000004。而电脑里存的“0.3”,又是另一个带着误差的近似值,两者恰好不相等。所以:

>>> 0.1 + 0.2 == 0.3False


03:那该怎么办?

侦探破了案,接下来就是“怎么办”。我给学生们介绍了三种办法。

第一种,比较的时候别用“==”,而是看两个数是不是“足够接近”。Python的math模块里有现成的函数:

>>> import math>>> math.isclose(0.1 + 0.2, 0.3)True

第二种,需要精确十进制计算的场合,比如算钱,用decimal模块,并且用字符串来创建:

>>> Decimal('0.1') + Decimal('0.2')Decimal('0.3')

有个学生眼尖,问:“为什么前面Decimal(0.1)那么长,这里Decimal('0.1')就是准的?”

好问题。Decimal(0.1)是把一个已经带着误差的float原封不动地“翻译”过来;Decimal('0.1')是直接按十进制的“0.1”来存,从一开始就没有误差。差别就在那一对引号上。

第三种,干脆避开小数。比如金额,很多系统都是以“分”为单位,用整数来存,1.23元存成123分,整数运算没有这个烦恼。

04:一个让我意外的结尾

下课前,我让大家写一句话,总结这节课学到了什么。

大部分同学写的是“浮点数有精度误差”“比较小数要用isclose”。

那个最先举手的男生,写的是:“电脑不是不会算,是它看世界的方式跟我们不一样。”

我把这句话念给全班听。教室里安静了一会儿,然后响起了掌声。

后来我常常想,这大概就是编程课最有意思的地方:一个看似“算错了”的结果,背后藏着计算机最底层的设计;而弄懂它的过程,也是在学着理解一种跟自己不一样的思维方式。

下次,如果你的程序里也冒出了一个奇怪的0.30000000000000004,别急着骂它。它只是在用二进制,诚实地告诉你:我已经尽力了。

作者介绍:
张一夫,佛山市顺德区乐从中学;
苏嘉健,华南师范大学教育科学学院
声明:
本文为 脚本之家专栏作者 投稿,未经允许请勿转载。
图片
  推荐阅读:
  1. 中国的程序员群体是否已经过多了?
  2. 朋友面试谈薪报价2w,HR非得压到1.9,还反问他:你就差这1000块钱?后来背调时问了他前同事十几个问题,对方最后直接挂了
  3. AI最危险的用法是帮着企业裁人!诺奖经济学家:编程最先被AI攻下来,是因为结果太容易检查
  4. 团队两个下属,一个天天加班,技术一般,但需求都能按时完成。另一个技术好,但会因为追求完美而延期,得盯着,A该给谁?
  5. 我们每天都在用的触摸屏,最早是谁发明的?

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/202819