金额为什么不用Float类型

在软件开发中,老鸟程序员常常告诫新鸟程序员,金额不要用Float类型,因为Float不精确,会产生误差,涉及到钱可能产生资损。但为什么Float类型不精确呢?这要从信息在计算机中的表示说起,请听我慢慢道来。

众所周知,计算机里的信息都是用二进制的 0 和 1 表示的,我们先看整数(Integer)在计算机里的表示,然后再看浮点数(Float)的表示。

整数的编码

计算机里存储整数要区分无符号整数(即正整数和0) 或有符合整数(即全部整数)。

无符号整数的编码

无符号整数的二进制数学表达式为:

无符号整数

其中 w 为位长(即一共有几位), xi为 0 或 1。例如:

[0101]2 = 0✖️23 + 1✖️22 + 0✖️21 + 1✖️20 = 5
[1011]2 = 1✖️23 + 0✖️22 + 1✖️21 + 1✖️20 = 11

有符号整数的编码

一般有符号整数的二进制表示使用补码方式,第一位为符号位(0表示正数,1表示负数)。数学表达式为:

有符号整数

其中 w 为位长,xw-1为第一位,其为 0 表示正数,为 1 表示负数。xi为 0 或 1。例如:

[0101]2 = -0✖️23 + 1✖️22 + 0✖️21 + 1✖️20 = 5
[1011]2 = -1✖️23 + 0✖️22 + 1✖️21 + 1✖️20 = -5

采用补码表示有符号整数的优点:可以用统一的加法运算方式处理无符号和有符号整数。

综上可知,用二进制表示整数不存在误差,都可以精确表示。

只要位数足够多就可以表示足够大的整数,但实际计算机的字长有限(通常32位或64位),所以计算过程中可能产生特别大的数(例如两个大数相乘,或者无符号大数转为有符号类型),超过了可表示的范围会产生溢出,变成另外一个数,这是在编程的时候需要特别注意的。

浮点数的编码

我们先看10进制表示小数的形式:
十进制小数

其中小数点 (“.”) 在 i=0 和 i=-1 之间。例如:

123.45 = 1✖️102 + 2✖️101 + 3✖️100 + 4✖️10-1 + 5✖️10-2 = 100 + 20 + 3 + 4/10 + 5/100 = 123 + 45/100 = 123.45

那么10进制小数这种表示方式有误差吗?答案是有的,例如表示 1/3 时,是个无限循环小数,必须进行舍入。

接下来我们看二进制小数的表示形式:
二进制小数

其中小数点 (“.”) 在 i=0 和 i=-1 之间。例如:

101.112 = 1✖️22 + 0✖️21 + 1✖️20 + 1✖️2-1 + 1✖️2-2 = 4 + 0 + 1 + 0.5 + 0.25 = 5.75

二进制小数只能表示可以被写成 x2y 的数,其他的数只能找离其最近的数代替,称之为舍入(rounding),例如我们常见的 10 进制舍入称为四舍五入。例如 10 进制的 0.2 (1/5) 就无法使用二进制小数精确表示,因为根号 5 是个无理数,这种情况只能按照精度要求舍入。下面是 Python 中令人困惑的浮点数运算(其他编程语言也类似)。

>>> 0.1 + 0.1 == 0.2
True
>>> 0.1 + 0.2 == 0.3
False

为什么 0.1 + 0.1 等于 0.2,但 0.1 + 0.2 不等于 0.3 呢?我们先看看小数求和运算后的结果:

>>> 0.1 + 0.1
0.2
>>> 0.1 + 0.2
0.30000000000000004

发现 0.1 + 0.1 是 0.2 ,但 0.1 + 0.2 却是 0.30000000000000004 。原因就是舍入造成的,0.1、0.2 和 0.3 都无法用二进制精确表示,这 3 个数在计算机存储的都是舍入后的结果,都是近似值。

所以上面诡异结果的原因是:近似的 0.1 加上舍入的 0.1 再舍入恰好等于近似的 0.2 ,但近似的 0.1 加上近似的 0.2 之后再舍入却运气没那么好,不等于近似的 0.3。也就是数值表示本身有误差,求和的过程中又加大了这种误差,造成了不等。

所以二进制不能精确表示10进制小数,有很多数都存在误差。因此软件开发中避免使用 Float 表示金额,避免用 == 来判断 Float 的相等性。

浮点数 IEEE 表示

最早的计算机厂商都是自己设计自己的浮点数表示规则,后来 IEEE 出了统一标准。标准都是对一类问题的经典解决方案,我们有必要了解下。

IEEE浮点标准用 V = (-1)s✖️M✖️2E 的形式表示一个数:

  • 符号 s=1 表示负数,s=0 表示正数;
  • 尾数 M 是一个二进制小数,范围是区间 [1, 2) ,或区间 [0, 1) ;
  • 阶码 E 是加权值。

在计算机中,一个字长的浮点数称为单精度数,两个字长的浮点数称为双精度数。表示浮点数的字节会分成三部分:

float存储结构
  • 位 s 与符号位 s 相同
  • 指数 e 与阶码 E相关,当 e 为全 1,表示无穷大。当 e 为 0 时,E = 1 - Bias ;当 e 大于 0 小于全 1 时, E = e - Bias,其中 Bias 为 2k-1 - 1 的偏移量 (k为e所占位长)
  • 小数 f 与 M 相关,当 e 为 0 时,M = f/2k-1 ; 当 e 大于 0 小于全 1 时,M = f/2k-1 + 1

下面我们以字长为 8 来举例,其中第1位为 s,第 2~5 为 e,第 6~8 为 f,偏移量 Bias 为 24-1 - 1 = 7。

0 0000 010

上例中 s 为 0, e 为 0,f 为 2,E = 1-7 = -6,M = f = 2/8,所以 V=(-1)0✖️2/8✖️2-6=1/256

0 0001 110

上例中 s 为 0, e 为 1,f 为 6,E = 1-7 = -6,M = 6/8+1 = 14/8,所以 V=(-1)0✖️14/8✖️2-6=7/256

0 1110 111

上例中 s 为 0, e 为 14,f 为 7,E = 14-7 = 7,M = 7/8+1 = 15/8,所以 V=(-1)0✖️15/8✖️27=1920/8

0 1111 000

上例中 s 为 0, e 为 全 1,表示无穷大。

金额应该使用什么类型?

至此我们知道了金额为什么不用 Float 类型,那么应该用什么类型呢?通常有两个方案选择:

1、使用 int 类型

如果金额计算精度要求不高,最小到分就行,而且最大金额也不超过 int 的范围,那么选 int 类型是最佳方案,存储金额的单位为分。int 可以精确表示,而且无论从存储还是计算,都更节省资源。

2、使用 decimal 类型

decimal 类型用来精确保存10进制小数,decimal 默认精确到小数点后 28 位,可以满足财务计算对精确的要求。

下面是Python里使用 decimal 的示例,可以看到不会出现浮点数的不等式。

>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
>>>

那么为什么 decimal 没有浮点数的问题呢? 这取决于它的存储方式,decimal 把整数和小数分开存储了,分开后小数按整数存储就可以用二进制精确表示了。

decimal 使用注意事项
构造 Decimal 类型可以使用 int 或 字符串,不要使用 float 构造 Decimal,否则构造出来的是精确的近似值。不过使用 float 构造decimal 可以回答上文中 “ 0.1 + 0.1 等于 0.2,但 0.1 + 0.2 不等于 0.3 ” 的诡异问题,详见以下示例:

>>> from decimal import *
>>> Decimal(0.1+0.1)
Decimal('0.200000000000000011102230246251565404236316680908203125')
>>> Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')
>>> Decimal(0.1+0.1) == Decimal(0.2)
True
>>> Decimal(0.1+0.2)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(0.1+0.2) == Decimal(0.3)
False
>>> 

总结

由于二进制的特殊性,我们没办法使用二进制精确的表示十进制的小数,所以为了不产生资损,金额尽量不使用 Float 类型表示,而按需求改为 Int 或 Decimal 类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容