文档章节

Python 的浮点数损失精度问题(为什么说双精度浮点数有15位十进制精度)

lionets
 lionets
发布于 2013/12/21 23:27
字数 3497
阅读 8878
收藏 15
点赞 2
评论 0

本篇讨论的现象可以从下面这段脚本体现出来:


   
>>> x = 0.0 >>> for i in range( 10 ): x += 0.1 print (x) 0.1 0.2 0.30000000000000004 0.4 0.5 0.6 0.7 0.7999999999999999 0.8999999999999999 0.9999999999999999 >>>

即:为什么有几行的输出看起来不对?

因为 Python 中使用双精度浮点数来存储小数。在 Python 使用的 IEEE 754 标准(52M/11E/1S)中,8字节64位存储空间分配了52位来存储浮点数的有效数字,11位存储指数,1位存储正负号,即这是一种二进制版的科学计数法格式。虽然52位有效数字看起来很多,但麻烦之处在于,二进制小数在表示有理数时极易遇到无限循环的问题。其中很多在十进制小数中是有限的,比如十进制的 1/10,在十进制中可以简单写为 0.1 ,但在二进制中,他得写成:0.0001100110011001100110011001100110011001100110011001…..(后面全是 1001 循环)。因为浮点数只有52位有效数字,从第53位开始,就舍入了。这样就造成了标题里提到的”浮点数精度损失“问题。 舍入(round)的规则为“0 舍 1 入”,所以有时候会稍大一点有时候会稍小一点。

Python 的浮点数类型有一个 .hex()方法,调用能够返回该浮点数的二进制浮点数格式的十六进制版本。这话听着有点绕,其实是这样的:本来浮点数应该有一个 .bin() 方法,用来返回其二进制浮点数格式。如果该方法存在的话,它看起来就像这样(p-4表示乘以 2-4,或者可以简单理解为小数点 左移 4 位):


   
>>> ( 0.1 ).bin() # 本方法其实并不存在 ' 1.1001100110011001100110011001100110011001100110011010p-4 '

但是这个字符串太长了,同时因为每 4 位二进制字符都可以换算成 1 位十六进制字符,于是Python就放弃了给浮点数提供 .bin() 方法,改为提供 .hex() 方法。这个方法将上面输出字符串的 52 位有效数字无损转换成了 13 位十六进制数字,所以实际存在的方法其实是这样的(注:二进制浮点数中小数点前的“1”不包含于那 52 位有效数字之中):


   
>>> ( 0.1 ).hex() ' 0x1.999999999999ap-4 '

前面的 0x 代表十六进制。p-4 没变,所以需要注意,这里的 p-4 还是二进制版的,也就是说在展开本格式的时候,你不能把小数点往左移 4 位,那样就相当于二进制左移 16 位了。前面提到过,小数点前这个“1”是不包含于 52 位有效数字之中的,但它确实是一个有效的数字呀,这是因为,在二进制浮点数中,第一位肯定是“1”,(是“0”的话就去掉这位,并在指数上-1)所以就不保存了,这里返回的这个“1”,是为了让人看懂而加上的,在内存的 8 位空间中并没有它。所以 .hex() 方法在做进制转换的时候,就没有顾虑到这个“1”,直接把 52 位二进制有效数字转换掉就按着原来的格式返回了。因此这个 .hex() 方法即使名义上返回的是一个十六进制数,它小数点前的那一位也只会是“1”,看下面示例:


   
>>> float.fromhex( ' 0x1.8p+1 ' ) == float.fromhex( ' 0x3.0p+0 ' ) True

一般我们用十六进制科学计数法来表示 3.0 这个数时,都会这么写“0x3.0p+0”。但是 Python 会这么写“0x1.8p+1”,即“1.1000”小数点右移一位变成“11.000”——确实还是 3.0 。就是因为这个 1 是直接遗传自二进制格式的。而我一开始没有理解这个 .hex() 的意义,还画蛇添足地自定义了一个 hex2bin() 方法,后来看看真是没必要啊~

而为了回应人们在某些状况下对这个精度问题难以忍受的心情(雾),Python 提供了另一种数字类型——Decimal 。他并不是内建的,因此使用它的时候需要 import decimal 模块,并使用 decimal.Decimal() 来存储精确的数字。这里需要注意的是:使用非整数参数时要记得传入一个字符串而不是浮点数,否则在作为参数的时候,这个值可能就已经是不准确的了:


   
>>> Decimal( 0.1 ) == Decimal( ' 0.1 ' ) False

在进一步研究到底损失了多少精度,或者说,八字节双精度浮点数最多可以达到多少精度的问题之前,先来整理一下小数和精度的概念。本篇中讨论的小数问题仅限于有理数范围,其实有理数也是日常编程中最常用到的数。有理数(rational number)一词派生于“比(ratio)”,因此并不是“有道理”的意思,而是指分数。有理数的内容扩展自自然数,由自然数通过有理运算(+ – * /)来得到的数系称为有理数,因此可以看到它较自然数扩充了:零、负整数和分数的部分。有理数总可以写成 p/q 的形式,其中 p、q 是整数且 q ≠ 0,而且当 p 和 q 没有大于 1 的公因子且 q 是正数的时候,这种表示法就是唯一的。这也就是有理数被称为 rational number 的原因,说白了就是分数。实际上 Python 的 float 类型还有一个 .as_integer_ratio() 的方法,就可以返回这个浮点数的最简分数表示,以一个元组的形式:


   
>>> ( 0.5 ).as_integer_ratio() ( 1 , 2 )

然后为了对有理数套用更直观的“位值法”表示形式,人们又开始用无限小数的形式表示有理数。而其中从某一位开始后面全是 0 的特殊情况,被称为有限小数(没错,无限小数才是本体)。但因为很多时候我们只需要有限位有效数字的精度就够用了,所以我们会将有理数保存到某一位小数便截止。后面多余小数的舍入方式便是“四舍五入”,这种方式较直接截断(round_floor)的误差更小。在二进制中,它表现为“0 舍 1 入”。当我们舍入到某一位以后,我们就可以说该数精确到了那一位。如果仔细体会每一位数字的含义就会发现,在以求得有限小数位下尽可能精确的值为目的情况下,直接截断的舍入方式其实毫无意义,得到的那最后一位小数也并不准确。例如,将 0.06 舍入成 0.1 是精确到小数点后一位,而把它舍入成 0.0 就不算。因此,不论是在双精度浮点数保留 52 位有效数字的时候,还是从双精度浮点数转换回十进制小数并保留若干位有效数字的时候,对于最后一位有效数字,都是需要舍入的。

插一句题外话:如何判断一个有理数在写成某种进制的小数时是否具有有限长度。就是看这个有理数的分母的质因子,是否全部包含于进制的质因子之中。举个栗子,1/2 这个数在二进制、四进制和八进制...中都是有限小数,但在三进制、五进制和七进制...中都是无限小数。同样道理,十进制下 1/n (n为整数,0<n<10) 这种形式的有理数只有 1/2,1/4,1/8 和 1/5 是有限小数。因此,曾经还有过数学家提议人类使用十二进制代替十进制的事情,因为 12 的质因子(2,3)比 10 的质因子(2,5)更小,也就可以表示更多的有限小数。

下图是一个(0,1)之间的数轴,上面用二进制分割,下面用十进制分割。比如二进制的 0.1011 这个数,从小数点后一位一位的来看每个数字的意义:开头的 1 代表真值位于 0.1 的右侧,接下来的 0 代表真值位于 0.11 的左侧,再接下来的 1 代表真值位于 0.101 的右侧,最后的 1 代表真值位于 0.1011 的右侧(包含正好落在 0.1011 上这种情况,这就是四舍五入的来源)。使用 4 位二进制小数表示的 16 个不同的值,除去 0,剩下的 15 个数字正好可以平均分布在(0,1)这个区间上,而十进制只能平均分布 9 个数字。显然 4 位二进制小数较于 1 位十进制小数将此区间划分的更细,即精度更高。

未标题-1_thumb[4]

把 0.1 的双精度版本(0x1.999999999999ap-4)展开成十进制。这里使用了 Decimal 类型,在给他赋值的时候,他会完整存储参数,但是要注意的是,使用 Decimal 进行运算是会舍入的,保留的位数由上下文决定。使用 decimal 模块的 getcontext() 方法可以得到上下文对象,其中的 prec 属性就是精度。下面还使用了 print() 方法,这是为了好看:


   
>>> print (Decimal( 0.1 )) 0.1000000000000000055511151231257827021181583404541015625

得到的这个十进制浮点数有效数字足有 55 位。虽然从二进制到十进制这个过程是完全精确的,但因为在存储这个二进制浮点数的时候进行了舍入,所以这个 55 位的十进制数,较于最初的 0.1 并不准确。至于到底能精确到原十进制数的哪一位,可以这么算: 2**53 = 9007199254740992 ≈ 10**16 ,(这里 53 算上了开头的“1”),即转换后的十进制小数的第 16 位有效数字很可能是精确的(第 15 位肯定是精确的)。换句话说,如果要唯一表示一个 53 位二进制数,我们需要一个 17 位的十进制数(但即使这样,也不代表对应的十进制和二进制数“相等”,他们只不过在互相转换的时候在特定精度下可以得到相同的的值罢了。就像上面例子中显示的,精确表示”0.1“的双精度版本,需要一个 55 位的十进制小数)。

不过可以看到,如果要保证转换回来的十进制小数与原值相等,那么只能保证到 15 位,第 16 位只是“很可能是精确的”。而且第 15 位的精确度也要依赖于第 16 位的舍入。实际上在 C++ 中,double 类型的十进制小数就是保留 15 位的(我从别处看来的,C++我自己并不熟悉)。所以如果 Python 的 float 类型的 __str__() 和 __repr__() 方法选择返回一个 15 位的小数,那么就不会出现本文讨论的第一个问题了。不论是早期的“0.10000000000000001”还是本文中出现的“0.30000000000000004”或者“0.7999999999999999”,我们可以看到它的不精确都是因为打印了过多位的有效数字——16 或 17 。假如强制 round 到 15 位的话:


   
a = 0.1 for i in range( 10 ): print (round(a, 15 )) a += 0.1 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0

我们再来看一下他的 16、17 位到底保存了什么:


   
>>> a = 0.0 >>> for i in range( 10 ): a += 0.1 print (a) print ( ' %.17f ' % a) print ( ' - ' * 19 ) 0.1 0.10000000000000001 ------------------- 0.2 0.20000000000000001 ------------------- 0.30000000000000004 0.30000000000000004 ------------------- 0.4 0.40000000000000002 ------------------- 0.5 0.50000000000000000 ------------------- 0.6 0.59999999999999998 ------------------- 0.7 0.69999999999999996 ------------------- 0.7999999999999999 0.79999999999999993 ------------------- 0.8999999999999999 0.89999999999999991 ------------------- 0.9999999999999999 0.99999999999999989 -------------------

上面短横线对齐的是第 17 位。虽然在这里第 16 位全部是精确的,但如果为了保证 100% 的准确率的话,还是需要舍入到第 15 位。另外一个细节,上面的例子其实有一个问题,就是使用 0.1++ 这种方式的时候,实际累加的是一个不精确的数字,所以有可能造成误差的放大。不过这里依然没有改正,是因为 0.5 那行,突然恢复真值了。这也不是因为后面藏了其他数字没有显示出来,我们来看一下:


   
>>> ' %.60f ' % ( 0.1 + 0.1 + 0.1 + 0.1 + 0.1 ) ' 0.500000000000000000000000000000000000000000000000000000000000 ' >>> print (Decimal( 0.1 + 0.1 + 0.1 + 0.1 + 0.1 )) 0.5

这里使用了一个格式限定符的示例。它的作用类似于 print Decimal。区别仅在于 Decimal 自己知道应该显示多少位,而格式化限定符不知道。(一般双精度浮点数转换过来不超过 100 位)。因为不打算继续深究了,所以就当这个“0.5”是个意外吧~如果想避免误差叠加,可以写成“i/10”的格式。

所以对于两种,不像十六进制和二进制般正好是指数关系的进制,永远都无法在各自的某一位上具有相同的精度。即 2m = 10n 这个等式没有使 m,n 同时为整数的解。但至少还可以构建一个精度包含的关系,比如上面 24 > 101 ,那么我们就说“4 位二进制精度高于 1 位十进制精度”从而通过 4 位二进制数转储 1 位十进制数的时候,总是精确的,反之则不然。同理根据这个不等式:1015 < 253 <1016 ,双精度浮点数的精度最高也就蕴含(不是等价)到十进制的 15 位了。另外虽然这种转化看起来浪费了很大的精度(第 16 位在很大概率上也是精确的)。有趣的是,210 = 1024,却和 103 = 1000 离的很近。因此一般我们可以通过这个 10:3 的比例来近似推导精度关系。

最后,因为浮点数的这点特性,在涉及到钱的地方都是不用浮点数的,他们会用定点数。即使用 ...xxxxx . xx 的格式存储数字,精确到小数点后第二位,即“分”。

© 著作权归作者所有

共有 人打赏支持
lionets
粉丝 90
博文 96
码字总数 131014
作品 0
朝阳
程序员
Python 浮点数运算

浮点数用来存储计算机中的小数,与现实世界中的十进制小数不同的是,浮点数通过二进制的形式来表示一个小数。在深入了解浮点数的实现之前,先来看几个 Python 浮点数计算有意思的例子: IEEE...

rainyear ⋅ 2016/05/12 ⋅ 0

浮点数加法引发的问题:浮点数的二进制表示

1、问题: 之前有同学问过这样一个问题: echo|awk '{print 3.99 -1.19 -2.80}'4.44089e-16 类似的问题还有在 java 或者 javascript 中: 23.53 + 5.88 + 17.64 = 47.05 23.53 + 17.64 + 5.8...

xrzs ⋅ 2013/08/26 ⋅ 0

为什么 PHP 和 JavaScript 取整 ((0.1+0.7)*10) 的结果不是 8?

php 代码 intval((0.7+0.1)*10) js 代码 parseInt((0.7+0.1)*10)上面的结果都等于 7 这是为什么? 为什么 0.2+0.6 等等就不会这样? 刚才测试了似乎跟语言没关系,所有语言都这样。 这和计算...

mickelfeng ⋅ 2013/06/18 ⋅ 1

MySQL类型float double decimal的区别

float数值类型用于表示单精度浮点数值,而double数值类型用于表示双精度浮点数值, float和double都是浮点型,而decimal是定点型; MySQL 浮点型和定点型可以用类型名称后加(M,D)来表示, ...

韩立伟 ⋅ 2017/06/30 ⋅ 0

javascript避免数字计算精度误差的方法详解

本篇文章主要是对javascript避免数字计算精度误差的方法进行了介绍,需要的朋友可以过来参考下,希望对大家有所帮助 如果我问你 0.1 + 0.2 等于几?你可能会送我一个白眼,0.1 + 0.2 = 0.3 啊...

土鳖的弟弟 ⋅ 2014/12/22 ⋅ 0

JavaScript四舍五入的那些坑

前言 经常使用JavaScript用来处理数字的程序员都知道,JavaScript的,这一函数,在格式化数字时,会自动进行四舍五入,例如: 但是在某些情况下,其四舍五入的结果,往往都不尽人意,例如: ...

光哥很霸气 ⋅ 2017/11/09 ⋅ 0

隐藏在 Node.js 浮点反序列化错误背后的故事

原文作者:孝达 在 Node.js 中,当我们把一个浮点数序列化,再反序列化: 我们会发现,再也取不出之前的值了: 然而,如果再序列化回去,发现结果还是相同的: 我勒个去!What happened? 排...

_朴灵_ ⋅ 05/14 ⋅ 0

据说有99%的人都会做错的面试题

这道题主要考察了面试者对浮点数存储格式的理解。另外,请不要讨论该题本身是否有意义之类的话题。本题只为了测试面试者相关的知识是否掌握,题目本身并没有实际的意义。 下面有6个浮点类型变...

androidguy ⋅ 2014/08/25 ⋅ 0

float浮点数的二进制存储方式及转换

int和float都是4字节32位表示形式。为什么float的范围大于int? float精度为6~7位。1.66*10^10的数字结果并不是166 0000 0000 指数越大,误差越大。 这些问题,都是浮点数的存储方式造成的。...

Playboy002 ⋅ 2015/11/03 ⋅ 0

python 中 print 函数用法大全

有一道ctf题为:ASCII码而已 u5927u5bb6u597duff0cu6211u662fu0040u65e0u6240u4e0du80fdu7684u9b42u5927u4ebauff01u8bddu8bf4u5faeu535au7c89u4e1du8fc7u767eu771fu7684u597du96beu3002u3002......

wt7315 ⋅ 2017/02/13 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

从零开始搭建Risc-v Rocket环境---(1)

为了搭建Rocke环境,我买了一个2T的移动硬盘,安装的ubuntu-16.04 LTS版。没有java8,gcc是5.4.0 joe@joe-Inspiron-7460:~$ java -version程序 'java' 已包含在下列软件包中: * default-...

whoisliang ⋅ 23分钟前 ⋅ 0

大数据学习路线(自己制定的,从零开始学习大数据)

大数据已经火了很久了,一直想了解它学习它结果没时间,过年后终于有时间了,了解了一些资料,结合我自己的情况,初步整理了一个学习路线,有问题的希望大神指点。 学习路线 Linux(shell,高并...

董黎明 ⋅ 29分钟前 ⋅ 0

systemd编写服务

一、开机启动 对于那些支持 Systemd 的软件,安装的时候,会自动在/usr/lib/systemd/system目录添加一个配置文件。 如果你想让该软件开机启动,就执行下面的命令(以httpd.service为例)。 ...

勇敢的飞石 ⋅ 31分钟前 ⋅ 0

mysql 基本sql

CREATE TABLE `BBB_build_info` ( `community_id` varchar(50) NOT NULL COMMENT '小区ID', `layer` int(11) NOT NULL COMMENT '地址层数', `id` int(11) NOT NULL COMMENT '地址id', `full_......

zaolonglei ⋅ 40分钟前 ⋅ 0

安装chrome的vue插件

参看文档:https://www.cnblogs.com/yulingjia/p/7904138.html

xiaoge2016 ⋅ 43分钟前 ⋅ 0

用SQL命令查看Mysql数据库大小

要想知道每个数据库的大小的话,步骤如下: 1、进入information_schema 数据库(存放了其他的数据库的信息) use information_schema; 2、查询所有数据的大小: select concat(round(sum(da...

源哥L ⋅ 今天 ⋅ 0

两个小实验简单介绍@Scope("prototype")

实验一 首先有如下代码(其中@RestController的作用相当于@Controller+@Responsebody,可忽略) @RestController//@Scope("prototype")public class TestController { @RequestMap...

kalnkaya ⋅ 今天 ⋅ 0

php-fpm的pool&php-fpm慢执行日志&open_basedir&php-fpm进程管理

12.21 php-fpm的pool pool是PHP-fpm的资源池,如果多个站点共用一个pool,则可能造成资源池中的资源耗尽,最终访问网站时出现502。 为了解决上述问题,我们可以配置多个pool,不同的站点使用...

影夜Linux ⋅ 今天 ⋅ 0

微服务 WildFly Swarm 管理

Expose Application Metrics and Information 要公开关于我们的微服务的有用信息,我们需要做的就是将监视器模块添加到我们的pom.xml中: 这将使在管理和监视功能得到实现。从监控角度来看,...

woshixin ⋅ 今天 ⋅ 0

java连接 mongo伪集群部署遇到的坑

部署mongo伪集群 #创建mongo数据存放文件地址mkdir -p /usr/local/config1/datamkdir -p /usr/local/config2/data mkdir -p /usr/local/config3/data mkdir -p /usr/local/config1/l......

努力爬坑人 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部