本文所有讨论都是基于python3版本,一般我们讨论到控制小数位数,会有以下几种方法:
- round(number, ndigits=None)
- num="%.3f"%number
- decimal模块
- format( )格式化
就会引出python四舍五入问题。
文章目录
- 一、python四舍五入问题
- 二、问题详解
- 三、自己写函数实现四舍五入
- 四、decimal模块
- 五、参考资料
一、python四舍五入问题
好多博客在说round做舍入的时候,要看舍入的前一位是奇数还是偶数,奇数进位,偶数舍去,其实是不准确的。例如:
>>> round(4.575,2)
4.58
>>> round(4.585,2)
4.58
>>> round(1.275,2)
1.27
前两个例子能用上面的“规则”解释,1.275的例子又做何解?
如果你用另外两个控制小数位数的办法,就会发现会遇到同样的问题。
>>> a="%.2f"%1.275
>>> a
'1.27'
>>> type(a)
<class 'str'>
>>>
>>> import decimal
#设置精度
>>> decimal.getcontext().prec=3
>>> b=decimal.Decimal(1.275)/decimal.Decimal(1)
>>> b
Decimal('1.27')
#format(1.275,'.2f')和下面的作用一样
>>> c='{:.2f}'.format(1.275)
>>> c
'1.27'
decimal如果想正确四舍五入,得用字符串作为Decimal( )的参数,decimal模块的内容在第四小节。
format有一些比较复杂的用法,详见python中的format函数
问题的根源来自于,浮点数的二进制表示和四舍五入的策略。
二、问题详解
首先是浮点数的二进制表示:
- float采用二进制编码描述浮点数。在二进制表示中,大多数有限位十进制小数无法使用二进制进行有限位精确表示。也就是说,有限位数的十进制小数,往往会变为无限位数的二进制小数。
- 对不能使用有限位二进制小数表示的十进制有限位小数,在系统中存储的是这些十进制浮点数的近似值。在近似值中,分为进位和截断两种类型,近似误差一般在左右。进位近似值大于原值,截断近似值小于原值。
- 表示为二进制近似值后,Python系统在进行round计算时,使用近似值,不是使用原值。
假设是浮点数的小数部分,那么十进制和二进制的表示分别如下:
1、十进制
2、二进制
python在使用和存储的时候,都是用和十进制最接近的数值。例如:
>>> format(1.275,'.20f')
'1.27499999999999991118'
再说回四舍五入的基本策略:
- 靠近最近(四舍六入);
- 距离相同时,保证最后一位是偶数;
做基本的四舍五入的时候,都是使用系统内表示的值,比如说1.275,系统内表示为1.27499999999999991118,所以四舍五入之后就是1.27;但还有些能够用二进制表示的小数部分,如果要舍入的那位是5,比如说1.125,四舍五入保留2位就是1.12(结果的最后一位是偶数)。
>>> format(1.125,'.20f')
'1.12500000000000000000'
>>> round(1.125,2)
1.12
>>> format(1.375,'.20f')
'1.37500000000000000000'
#结果的最后一位是偶数
>>> round(1.375,2)
1.38
很明显,因为python保存的值不一定和我们想要的值相同,如果设及精度比较高的计算(float表示16位精度),就需要自己写方法或者使用decimal模块。
三、自己写函数实现四舍五入
你可以自己写一个函数:
def new_round(num,ndigits=0):
number=str(num).split('.')
if int(number[0])<0:
result='-'
number[0]=number[0][1:]
if ndigits==0:
if int(number[1][0])>=5:
result+=str(int(number[0])+1)
else:
result+=number[0]
elif ndigits>0:
n=ndigits-len(number[1])+1
number[1]+='0'*n
if int(number[1][ndigits])>=5:
result+=number[0]+'.'+str(int(number[1][0:ndigits])+1)
else:
result+=number[0]+'.'+number[1][0:ndigits]
else:
if -ndigits>=len(number[0]):
result='0'
elif int(number[0][ndigits])>=5:
result+=str(int(number[0][0:ndigits])+1)+'0'*(-ndigits)
else:
result+=number[0][0:ndigits]+'0'*(-ndigits)
return result
保留的位数可以是负数,正确性待检验。
四、decimal模块
decimal模块为快速正确舍入的十进制浮点运算提供支持。官网介绍的比较详细:decimal
decimal算数上下文可以设置精度和舍入的规则:
>>> import decimal
>>> decimal.getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
默认精度是28,默认舍入规则是ROUND_HALF_EVEN。舍入规则一共有8种:
示例:
>>> decimal.getcontext().rounding='ROUND_HALF_DOWN'
>>> decimal.getcontext().prec=3
>>>
>>> decimal.Decimal(1)/decimal.Decimal(3)
Decimal('0.333')
>>> decimal.Decimal(1.275)/decimal.Decimal(1)
Decimal('1.27')
>>> decimal.getcontext().rounding='ROUND_HALF_EVEN'
#Decimal( )可以接受字符串为参数,可以避免float类型的“乱码”
>>> decimal.Decimal(1.275)/decimal.Decimal(1)
Decimal('1.27')
>>> decimal.Decimal('1.275')/decimal.Decimal(1.000)
Decimal('1.28')
>>> decimal.getcontext().prec=20
>>> decimal.Decimal('1.275')
Decimal('1.275')
>>> decimal.Decimal(1.275)
Decimal('1.274999999999999911182158029987476766109466552734375')
五、参考资料
Python四舍五入问题详解