当前位置 主页 > 网站技术 > 代码类 >

    Python浮点数四舍五入问题的分析与解决方法

    栏目:代码类 时间:2019-11-19 21:08

    问题

    昨天遇到一个问题,在 6.6045 保留三位小数时,使用 round() 函数进行计算,我们希望得到 6.605,然而:

    >>> round(6.6045, 3)
    6.604

    网上有人说,因为在计算机里面,小数是不精确的,例如 1.115 在计算机中实际上是 1.114999999999999991182,所以当你对这个小数精确到小数点后两位的时候,实际上小数点后第三位是 4,所以四舍五入,结果为 1.11.

    这种说法,对了一半。

    因为并不是所有的小数在计算机中都是不精确的。例如 0.125 这个小数在计算机中就是精确的,它就是 0.125,没有省略后面的值,没有近似,它确确实实就是 0.125.

    但是如果我们在 Python 中运行:

    >>> round(0.125, 2)
    0.12

    为什么在这里四舍了?

    还有更奇怪的,另一个在计算机里面能够精确表示的小数 0.375,我们来看看精确到小数点后两位是多少:

    >>> round(0.375, 2)
    0.38

    为什么在这里又五入了?

    解析

    因为在 Python3 里面,round 对小数的精确度采用了四舍六入五成双的方式。

    如果你写过大学物理的实验报告,那么你应该会记得老师讲过,直接使用四舍五入,最后的结果可能会偏高,所以需要使用奇进偶舍的处理方法。

    例如对于一个浮点数 a.bcd,需要精确到小数点后两位,那么就要看小数点后第三位:

    如果 d 小于 5,直接舍去; 如果 d 大于 5,直接进位; 如果 d 等于 5:
    d 后面没有数据,且 c 为偶数,那么不进位,保留 c
    d 后面没有数据,且 c 为奇数,那么进位,c 变成 (c + 1) 如果 d 后面还有非 0 数字,例如实际上小数为 a.bcdef,此时一定要进位,c 变成 (c + 1)

    关于奇进偶舍,有兴趣的朋友可以在维基百科搜索这两个词条:数值修约和奇进偶舍。

    所以,round 给出的结果如果跟设想的不一样,那么需要考虑两个原因:
    你的这个小数在计算机中能不能被精确储存?如果不能,那么它可能并没有达到四舍五入的标准,例如 1.115,它的小数点后第三位实际上是 4,当然会被舍去。
    如果你的这个小数在计算机中能被精确表示,那么,round 采用的进位机制是奇进偶舍,所以这取决于你要保留的那一位,它是奇数还是偶数,以及它的下一位后面还有没有数据。

    关于奇进偶舍,有兴趣的朋友可以在搜索这两个词条:数值修约和奇进偶舍。

    回到最开始的问题,对于 6.6045 这个浮点数,我们在 Scheme 中查看一下它的精确形式:

    > (exact 6.6045)
    3718002967371055/562949953421312

    也就是说它是不能被精确储存的,大概表现为 6.60449999999999…的形式,因此四舍五入的时候得到了 6.604。

    如何正确进行四舍五入

    如果要实现数学上的四舍五入,那么就需要使用 decimal 模块,具体用法参考官方文档:https://docs.python.org/zh-cn...。
    其中 quantize 的函数原型和文档说明,提到了可以通过指定 rounding 参数来确定进位方式。如果没有指定 rounding 参数,那么会默认使用上下文提供的进位方式。

    现在我们来查看一下默认的上下文中的进位方式是什么:

    >>> from decimal import getcontext
    >>> getcontext().rounding
    'ROUND_HALF_EVEN'

    ROUND_HALF_EVEN 实际上就是奇进偶舍,如果要指定真正的四舍五入,那么我们需要在 quantize 中指定进位方式为 ROUND_HALF_UP:

    >>> from decimal import Decimal, ROUND_HALF_UP
    >>> Decimal('0.125').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('0.13')