本文所有讨论都是基于python3版本,一般我们讨论到控制小数位数,会有以下几种方法:

  1. round(number, ndigits=None)
  2. num="%.3f"%number
  3. decimal模块
  4. 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函数

问题的根源来自于,浮点数的二进制表示和四舍五入的策略。

二、问题详解

首先是浮点数的二进制表示:

  1. float采用二进制编码描述浮点数。在二进制表示中,大多数有限位十进制小数无法使用二进制进行有限位精确表示。也就是说,有限位数的十进制小数,往往会变为无限位数的二进制小数。
  2. 对不能使用有限位二进制小数表示的十进制有限位小数,在系统中存储的是这些十进制浮点数的近似值。在近似值中,分为进位和截断两种类型,近似误差一般在8位小数点 python python怎么规定小数位数_8位小数点 python左右。进位近似值大于原值,截断近似值小于原值。
  3. 表示为二进制近似值后,Python系统在进行round计算时,使用近似值,不是使用原值。

假设8位小数点 python python怎么规定小数位数_8位小数点 python_02是浮点数的小数部分,那么十进制和二进制的表示分别如下:

1、十进制
8位小数点 python python怎么规定小数位数_四舍五入_03
2、二进制
8位小数点 python python怎么规定小数位数_git_04
python在使用和存储的时候,都是用和十进制最接近的数值。例如:

>>> format(1.275,'.20f')
'1.27499999999999991118'

再说回四舍五入的基本策略:

  1. 靠近最近(四舍六入);
  2. 距离相同时,保证最后一位是偶数;

做基本的四舍五入的时候,都是使用系统内表示的值,比如说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种:

8位小数点 python python怎么规定小数位数_git_05


示例:

>>> 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四舍五入问题详解