使用 Python 标准库中数字和数学模块 >>>> decimal 模块 ,实现 Python 浮点数精确运算,可以满足会计方面的应用和有高精度要求的应用场景。
前记 前面的博文 [ >>>> 编程基础之进制详解 <<<< ] 中提到过,在计算机中浮点数并不能准确的表示十进制,这是由于十进制小数在转换成计算机可识别、处理的二进制小数时,存在无限问题,此时计算机会在某个精度上直接进行舍入,以至于运算还没开始,一个很小的舍入错误就已经产生了。
而有误差的两个数,其运算的结果,很可能和我们的期望不一样了,例如:0.1 + 0.2 > 0.3
。详情可参见博文:[ >>>> 为什么 0.1 + 0.2 不等于 0.3? <<<< ]。
并且在运算(即便最简单的数学运算)中也会由于近似处理、内存限制等原因出现舍入误差。同时,上述种种的舍入误差会在后续的计算过程中不断传递累计,最终带来不可控制的后果。
Python 近似机制 这一小节的目的是认识一下 Python3 中浮点数的近似进制。
先不要运行 round(1.115)
,考虑一下其结果是 1.11
还是 1.12
???
悄悄告诉你是(1.11),你猜对了么?!!
计算机中大多数小数是不精确的,例如 1.115
在计算机中实际上是 1.1149999999999999911182
。当你对这个小数精确到小数点后两位的时候,实际上小数点后第三位是 4
,所以四舍五入的结果为 1.11
。
再来看一个在计算机中可精确表示的小数 0.125
(不要问为什么是精确的,好好看前记中提供的博文外链),考虑一下 round(0.125)
结果是 0.12
还是 0.13
???
运行后你会发现,把 0.125
精确到小数点后两位,四舍五入结果是 0.12
,为什么这里四舍了???
再来尝试一下 round(0.375)
,其四舍五入结果是 0.38
,这里为什么又五入了?!!
事实上 >>>>
Python3 中,round
对小数的精确度采用了 “四舍六入五成双 ” 的方式,需要使用 “奇进偶舍 ” 的处理方法进行判断。
舍入判断规则 >>>>
[1] >>>> 对于一个小数 a.bcd
,如需要精确到小数点后两位,那么就要看小数点后第三位。
[2] >>>> 如果 d <= 4
,直接舍去;如果 d >= 6
,直接进位。
[3] >>>> 如果 d==5
>>>> 1)d
后面没有数据 && c
为偶数:那么不进位,保留 c
;2)d
后面没有数据 && c
为奇数:那么进位,c
—> c + 1
;3)d
后面有数据,直接进位,c
—> c + 1
。
谈到 Python 的近似机制,主要是由于在实际应用场景中都会涉及到精确度(近似)问题。就 Python 的浮点数运算而言,大多数计算机每次计算误差不会超过 2^53,但这对于大多数任务来说已经足够了。也就是说,我们允许(对结果无关紧要的)误差存在。
然而 >>>>
对于金融方面的应用和有高精度要求的应用场景,舍入误差就显得尤为明显了,这就需要用到 Python 中的 decimal 模块了。
decimal Module decimal 模块中,可以通过 整数,字符串,元组,或 浮点数 ,来构建 decimal.Decimal
对象,以实现精确计算。
先来看一个简单演示 —> 如何使用 decimal 模块,使得 0.1 + 0.2 == 0.3
(进行精确计算)???方法如下:
1 2 3 4 5 6 7 8 from decimal import Decimala = Decimal('0.1' ) b = Decimal('0.2' ) sum = a + bprint(sum , type (sum ))
如果是浮点数,特别注意因为浮点数本身存在误差,需要先将浮点数转化为字符串,再传入 Decimal。
易错点 容易出错的是 >>>>
[1] >>> 浮点数直接参与构建 Decimal 对象
1 2 3 4 5 6 7 8 from decimal import Decimala = Decimal(0.1 ) b = Decimal(0.2 ) sum = a + bprint(sum , type (sum ))
可以看到,此时浮点数未转换为字符串,直接用于构造 Decimal 对象,相当于没有用到精确计算。此时使用的是 0.1
&& 0.2
在计算机中的精确值,,可以打印出来看一下:
1 2 3 4 5 6 7 8 9 from decimal import Decimala = Decimal(0.1 ) print(a) b = Decimal(0.2 ) print(b)
你可以想到的是:如果一个小数在计算机中是精确的,那么直接使用(未转换为字符串)其构建的 Decimal 对象值刚好就是小数本身的值。
[2] >>> 混合操作 Decimal 和浮点数
直接看样例:
1 2 3 4 5 6 from decimal import Decimala = Decimal('0.1' ) b = 0.125 print(a + b)
Decimal 数值对象可以直接和整型(int)、布尔型(bool)数值进行计算。与浮点数或分数模块 fractions.Fraction
实例在算术运算中结合使用时会报错:TypeError(例如尝试将 Decimal 加到 float)。
但是,可以使用 Python 的比较运算符来比较 Decimal 实例 x 和另一个数字 y(浮点数也可以参与比较了)。
设置有效位数 当使用十进制模块运算过程中出现无限小数时,可以通过设置 有效数字 来达到需要的目标精度:
[1] >>> decimal.getcontext().prec
1 2 3 4 5 from decimal import Decimal, getcontextgetcontext().prec = 6 print(Decimal(1 )/Decimal(7 )) print(1 /7 )
[2] >>> quantize(Decimal(‘0.000’))
在截取有效位数时,你也可以使用 quantize 方法来决定 保留几位小数 ,同时支持自定义四舍五入(舍入)机制,:
1 2 3 4 5 6 7 8 9 10 11 12 13 from decimal import Decimal, ROUND_DOWNres = Decimal(1 )/Decimal(7 ) print(res) res1 = res.quantize(Decimal('0.000' )) print(res1) res2 = res.quantize(Decimal('0.000' ), rounding=ROUND_DOWN) print(res2) print(res.quantize(Decimal('.1' ))) print(res.quantize(Decimal('.01' ))) print(res.quantize(Decimal('1.' )))
你需要注意:保留有效数字 && 保留小数点后几位 的区别!!!
| >>>> ===================== 需要注意的是 ====================== <<<< |
精度提升的同时,必然带来的是性能的损失。 在对数据要求特别精确的场合(例如财务结算),这些性能的损失是值得的。 但是如果是大规模的科学计算,就需要考虑运行效率了。毕竟原生的 float
比 Decimal
对象肯定是要快很多的。
想要更好、更准确、更深入的使用 decimal 模块,你需要了解本章节内容,或者移步至 >>>> Python decimal 模块官方说明文档 <<<< 进行学习。
You Need Know More Python 提供了 decimal 模块,用于支持正确舍入的十进制浮点算术。它具有如下特点:
[1] >>>> Decimal 数字的表示是完全精确的
相较于浮点数据类型,像 1.1
&& 1.2
这样的数字在二进制浮点中是没有精确表示的,最终用户通常不希望 1.1 + 2.2
如二进制浮点数表示那样被显示为 3.3000000000000003
。
[2] >>>> 数字表示的精确性会延续到算术类操作中
对于 decimal 浮点数,0.1 + 0.1 + 0.1 - 0.3
会精确地等于零。 而对于二进制浮点数,结果则为 5.5511151231257827e-017
。 虽然接近于零,但其中的误差将妨碍可靠的相等性检验,并且误差还会不断累积。 因此,decimal 更适合具有严格相等不变性要求的会计类应用。
[3] >>>> decimal 模块包含有效位的概念
Decimal 数字在计算后,会保留尾随零以表示有效位。例如:1.30 + 1.20 = 2.50
&& 1.3 * 1.2 = 1.56
&& 1.30 * 1.20 = 1.5600
等。
[4] >>>> decimal 模块支持用户自定义精度(默认是 28 位)
1 2 3 4 5 6 7 from decimal import Decimal, getcontextprint(Decimal(1 ) / Decimal(7 )) getcontext().prec = 6 print(Decimal(1 ) / Decimal(7 ))
[5] >>>> decimal 模块旨在支持“无偏差,精确无舍入的十进制算术(有时称为定点数算术)和有舍入的浮点数算术”
decimal 模块的设计以三个概念为中心:decimal 数值(Decimal) ,算术上下文(Context) ,以及 信号 。
decimal 数值(Decimal) decimal 数值是不可变对象,它由符号,系数和指数位组成。并且为了保持有效位,系数位不会截去末尾零。
[1] >>>> 构建 Decimal 实例
前面我们提到过,可以基于整数、字符串、浮点数或元组构造 Decimal 实例。decimal 数值中还可以是一些特殊值,例如 Infinity
,-Infinity
和 NaN
,还区分 -0
和 +0
。
来看一些构建实例 >>>>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from decimal import Decimalprint(Decimal(10 )) print(Decimal('3.14' )) print(Decimal(3.14 )) print(Decimal((0 , (3 , 1 , 4 ), -2 ))) print(Decimal(str (2.0 ** 0.5 ))) print(Decimal(2 ) ** Decimal('0.5' )) print(Decimal('NaN' )) print(Decimal('-Infinity' )) print(Decimal('-0' ))
如果使用 tuple 进行构建,它应该有三个组件:一个符号( 0 表示正数或 1 表示负数),一个数字的 tuple 和整数指数。 例如:Decimal((0, (1, 4, 1, 4), -3)) 返回 Decimal(‘1.414’)。
[2] >>>> decimal 模块可以和 Python 其它部分很好交互。
直接来看实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 > >> data = list(map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25' .split())) > >> max(data) Decimal('9.25') > >> min(data) Decimal('0.03') > >> sorted(data) [Decimal('0.03'), Decimal('1.00'), Decimal('1.34'), Decimal('1.87'), Decimal('2.35'), Decimal('3.45'), Decimal('9.25')] > >> sum(data) Decimal('19.29') > >> a,b,c = data[:3] > >> str(a) '1.34' > >> float (a) 1.34 > >> round(a, 1) Decimal('1.3') > >> int(a) 1 > >> a * 5 Decimal('6.70') > >> a * b Decimal('2.5058') > >> c % a Decimal('0.77')
Decimal 也可以使用一些数学函数:
1 2 3 4 5 6 7 8 9 > >> getcontext().prec = 28 > >> Decimal(2).sqrt() Decimal('1.414213562373095048801688724') > >> Decimal(1).exp() Decimal('2.718281828459045235360287471') > >> Decimal('10' ).ln() Decimal('2.302585092994045684017991455') > >> Decimal('10' ).log10() Decimal('1')
[3] >>>> 构建的 Decimal 的重要性仅由输入的数值决定, 上下文精度和舍入仅在算术运算期间发挥作用。
例如,上面我们通过 getcontext().prec = 6
这样的语句设置过精度,这就是通过算术上下文实现的。它无法影响一个新创建的 Decimal 对象的存储位数。
这里不理解也没关系,你可以通过先学习下面的【算术上下文(Context)】之后,再来进行理解学习。
1 2 3 4 5 6 7 8 9 10 11 12 13 from decimal import Decimal, getcontextgetcontext().prec = 6 print(Decimal('3.0' )) print(Decimal('3.1415926535' )) print(Decimal('3.1415926535' ) + Decimal('2.7182818285' ))
后面你还会学到 getcontext().rounding = ROUND_UP
这样的语句,来设置运算时的舍入规则。它也仅在运算时生效。
1 2 3 4 5 6 7 8 9 10 11 from decimal import Decimal, getcontextgetcontext().prec = 6 getcontext().rounding = ROUND_UP print(Decimal('3.1415926535' )) print(Decimal('3.1415926535' ) + Decimal('2.7182818285' ))
算术上下文(Context) 算术上下文是指算术运算所在的环境 >>>> 用于管理 decimal 模块算术运算期间的,精度、舍入规则、指数的限制范围,以及确定将哪些信号视为异常。
[1] >>>> getcontext() 函数
你可以通过 getcontext()
来查看当前的上下文环境,并在必要时为精度、舍入或启用的陷阱设置新值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from decimal import getcontextprint(getcontext()) getcontext().prec = 7 getcontext().rounding=ROUND_DOWN
其中,上下文中支持的舍入(rounding)规则选项全集以及其解释如下:
1 2 3 4 5 6 7 8 9 # 这里不用深究其参数含义,后续会通过实例进行说明 ROUND_CEILING (towards Infinity), ROUND_DOWN (towards zero), ROUND_FLOOR (towards -Infinity), ROUND_HALF_DOWN (to nearest with ties going towards zero), ROUND_HALF_EVEN (to nearest with ties going to nearest even integer), ROUND_HALF_UP (to nearest with ties going away from zero), or ROUND_UP (away from zero). ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero)
如上所示,getcontext()
函数访问当前上下文并允许更改设置,大多数程序仅在程序开始时调整当前上下文一次,这种方法满足大多数应用程序的需求。
[2] >>>> setcontext() 函数 && Context() 构建函数
而对于更复杂的任务,例如你需要创建一个备用的上下文环境以便在某些情况下使用,你可以通过 Context()
构造函数创建一个备用的上下文,并且通过 setcontext()
函数来修改当前上下文。
Context 构造函数格式:decimal.Context(prec=None, rounding=None, Emin=None, Emax=None, capitals=None, clamp=None, flags=None, traps=None)
1 2 3 4 5 6 7 8 9 10 11 12 13 from decimal import Decimal, getcontext, setcontext, Context, ROUND_UPprint(getcontext()) print(Decimal(1 ) / Decimal(7 )) myothercontext = Context(prec=40 , rounding=ROUND_UP) setcontext(myothercontext) print(Decimal(1 ) / Decimal(7 )) print(getcontext())
并且,decimal 模块提供了两个现成的标准上下文 BasicContext
和 ExtendedContext
。
decimal.BasicContext:精度设为 9
;舍入设为 ROUND_HALF_UP
;清除所有旗标;启用所有陷阱(视为异常),但 Inexact
, Rounded
和 Subnormal
除外(由于启用了许多陷阱,此上下文适用于进行调试)。
decimal.ExtendedContext:精度设为 9
;舍入设为 ROUND_HALF_EVEN
;清除所有旗标;不启用任何陷阱(因此在计算期间不会引发异常)。由于禁用了陷阱,此上下文适用于希望结果值为 NaN
或 Infinity
而不是引发异常的应用。 这允许应用在出现当其他情况下会中止程序的条件时仍能完成运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from decimal import Decimal, getcontext, setcontext, ExtendedContext, BasicContextprint(getcontext()) print(Decimal(1 ) / Decimal(7 )) print(ExtendedContext) setcontext(ExtendedContext) print(Decimal(1 ) / Decimal(7 )) print(Decimal(23 ) / Decimal(0 )) print(BasicContext) Context(prec=9 , rounding=ROUND_HALF_UP, Emin=-999999 , Emax=999999 , capitals=1 , clamp=0 , flags=[], traps=[Clamped, InvalidOperation, DivisionByZero, Overflow, Underflow]) setcontext(BasicContext) print(Decimal(23 ) / Decimal(0 ))
信号 信号是 >>>> 在算术过程中可能出现的 异常条件组 。并且根据应用程序的需要,信号可能会被忽略,被视为信息,或被视为异常。
decimal 模块中的信号有:Clamped
、InvalidOperation
、DivisionByZero
、Inexact
、Rounded
、Subnormal
、Overflow
、Underflow
以及 FloatOperation
。
对于每个信号,都有一个标志(flag)和一个陷阱启动器(trap)。遇到信号时,其标志会被设置为 1 ;然后,如果相应陷阱启用器也设置为 1,则引发异常。需要注意的是,标志是粘性的,因此用户需要在监控计算之前重置它们。
[1] >>>> 如何查看检测到的信号标志,以及启动相应陷阱:
并且,信号的标志(flags)以及陷阱启动器(trips)设置,也是由算术上下文进行管理的,分别对应 Context
中的 flags
&& traps
字段。
例如,在标准上下文 ExtendedContext
中,使用上下文的 traps 字段中的字典设置单个陷阱 DivisionByZero
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from decimal import Decimal, getcontext, setcontext, DivisionByZerosetcontext(ExtendedContext) print(getcontext()) print(Decimal(1 ) / Decimal(0 )) print(getcontext()) getcontext().traps[DivisionByZero] = 1 print(getcontext()) print(Decimal(1 ) / Decimal(0 ))
另外,设置 FloatOperation
陷阱之后,如果其信号被捕获,Decimal 构造函数中的小数和浮点数的意外混合或排序比较会引发异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from decimal import Decimal, getcontext, FloatOperationc = getcontext() c.traps[FloatOperation] = True print(c) Decimal(3.14 ) Decimal('3.5' ) < 3.7 Decimal('3.5' ) == 3.5
[2] >>>> 信号粘性标志清除
我们知道,上下文环境可以监视到计算期间遇到的异常情况的信号标志,由于标志的粘性,其会保持设置直到明确清除。你可以使用 clear_flags()
方法清除每组受监控计算之前的标志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from decimal import Decimal, getcontext, ExtendedContextprint(getcontext()) print(Decimal(355 ) / Decimal(113 )) print(getcontext()) getcontext().clear_flags() print(getcontext())
Python 浮点数和十进制模块测试 这一小节,我们通过具体的代码来比较浮点数(float)和十进制(decimal)模块的差异:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 from decimal import Decimal, getcontextfrom decimal import ROUND_UP, ROUND_DOWN, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_05UPx = 4.20 y = 2.10 a = 1.10 b = 2.30 def float_calculate (): sum = x + y print(sum ) if sum == 6.30 : print("Floating-point number addition is a precise calculations." ) else : print("Floating-point number addition is a Inexact operations." ) sub = a - b print(sub) if sub == -1.2 : print("Floating-point number subtraction is a precise calculations." ) else : print("Floating-point number subtraction is a Inexact operations." ) mul = x * a print(mul) if mul == 4.62 : print( "Floating-point number multiplication is a precise calculations." ) else : print("Floating-point number multiplication is a Inexact operations." ) div = x / 0.7 print(div) if div == 6.0 : print("Floating-point number division is a precise calculations." ) else : print("Floating-point number division is a Inexact operations." ) e_round = round (195.00 / 24 , 2 ) print(e_round) if e_round == 8.13 : print("Floating-point number rounding is a precise operations." ) else : print("Floating-point number rounding is a Inexact operations." ) def decimal_calculate (): getcontext().prec = 3 sum = Decimal(str (x)) + Decimal(str (y)) print(type (sum ), sum ) e_result = Decimal("195.00" ) / Decimal("24" ) print(e_result) def float_info (): i = 1.115 j = 0.125 k = 0.1251 m = 0.375 print("round(1.115) >>>>" , round (i, 2 )) print("round(0.125) >>>>" , round (j, 2 )) print("round(0.1251) >>>>" , round (k, 2 )) print("round(0.375) >>>>" , round (m, 2 )) def decimal_info (): i = 1.115 j = 0.125 k = 0.1251 m = 0.375 print("浮点数数据不精确 >>>" , Decimal(i)) print("浮点数数据精确 >>>" , Decimal(i)) print("默认上下文环境 >>>" , getcontext()) print('传入的是浮点型默认方式四舍五入:' , Decimal(i).quantize(Decimal('0.00' ))) print('传入的是浮点型默认方式四舍五入:' , Decimal(j).quantize(Decimal('0.00' ))) print('传入的是浮点型默认方式四舍五入:' , Decimal(k).quantize(Decimal('0.00' ))) print('传入的是浮点型默认方式四舍五入:' , Decimal(m).quantize(Decimal('0.00' ))) print('传入的是字符串默认方式四舍五入:' , Decimal(str (i)).quantize(Decimal('0.00' ))) print('传入的是字符串真实四舍五入:' , Decimal(str (i)).quantize(Decimal('0.00' ), ROUND_HALF_UP)) print('传入的是字符串真实四舍五入:' , Decimal(str (j)).quantize(Decimal('0.00' ), ROUND_HALF_UP)) print('传入的是字符串真实四舍五入:' , Decimal(str (k)).quantize(Decimal('0.00' ), ROUND_HALF_UP)) print('传入的是字符串真实四舍五入:' , Decimal(str (m)).quantize(Decimal('0.00' ), ROUND_HALF_UP)) print("传入的是字符串向上取值:" , Decimal("0.121" ).quantize(Decimal("0.00" ), ROUND_UP)) print("传入的是字符串向下取值:" , Decimal("0.129" ).quantize(Decimal("0.00" ), ROUND_DOWN)) print("传入的是字符串向上真实四舍五入:" , Decimal("0.125" ).quantize(Decimal("0.00" ), ROUND_HALF_UP)) print("传入的是字符串向下四舍五入:" , Decimal("0.125" ).quantize(Decimal("0.00" ), ROUND_HALF_DOWN)) print('传入的是字符串默认方式四舍五入:' , Decimal('0.125' ).quantize( Decimal('0.00' ), ROUND_HALF_EVEN)) print('传入的是字符串默认方式四舍五入:' , Decimal('0.375' ).quantize( Decimal('0.00' ), ROUND_HALF_EVEN)) print('传入的是字符串精确位数字为 5 入:' , Decimal('5.351' ).quantize( Decimal('0.00' ), ROUND_05UP)) print('传入的是字符串精确位数字为 0 入:' , Decimal('5.301' ).quantize( Decimal('0.00' ), ROUND_05UP)) print('传入的是字符串精确位数字非 0 和 5 舍:' , Decimal('5.399' ).quantize( Decimal('0.00' ), ROUND_05UP)) print('传入的是字符串正数时与 ROUND_UP 一致:' , Decimal('5.391' ).quantize(Decimal('0.00' ), ROUND_CEILING)) print('传入的是字符串正数时与 ROUND_DOWN一致:' , Decimal('5.399' ).quantize(Decimal('0.00' ), ROUND_FLOOR)) print('传入的是字符串负数时与 ROUND_DOWN 一致:' , Decimal('-5.399' ).quantize(Decimal('0.00' ), ROUND_CEILING)) print('传入的是字符串负数时与 ROUND_UP 一致:' , Decimal('-5.399' ).quantize(Decimal('0.00' ), ROUND_FLOOR)) print('传入的是字符串与正负无关,最后一位始终进:' , Decimal('-5.399' ).quantize(Decimal('0.00' ), ROUND_UP)) if __name__ == "__main__" : decimal_info()