V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
itskingname
V2EX  ›  分享创造

Python 3 中 round 进位不准确,并非只是因为浮点数二进制不准确

  •  3
     
  •   itskingname · 2019-03-31 13:27:29 +08:00 · 3888 次点击
    这是一个创建于 2066 天前的主题,其中的信息可能已经有所发展或是发生改变。

    round 到底出了什么问题?

    我们来说说,在 Python 3 里面,round这个内置的函数到底有什么问题。

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

    这种说法,对了一半。

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

    但是如果我们在 Python 中把0.125精确到小数点后两位,那么它的就会变成0.12

    >>> round(0.125, 2)
    0.12
    

    为什么在这里四舍了?

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

    >>> round(0.375, 2)
    0.38
    

    为什么这里又五入了?

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

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

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

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

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

    所以,round给出的结果如果与你设想的不一样,那么你需要考虑两个原因:

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

    如何正确进行四舍五入

    如果要实现我们数学上的四舍五入,那么就需要使用 decimal 模块。

    如何正确使用 decimal 模块呢?

    看官方文档!!!

    看官方文档!!!

    看官方文档!!!

    不要担心看不懂英文,Python 已经推出了官方中文文档(有些函数的使用方法还没有翻译完成)。

    我们来看一下: https://docs.python.org/zh-cn/3/library/decimal.html#decimal.Decimal.quantize

    官方文档给出了具体的写法:

    >>>Decimal('1.41421356').quantize(Decimal('1.000'))
    Decimal('1.414')
    

    那么我们来测试一下,0.1250.375分别保留两位小数是多少:

    >>> from decimal import Decimal
    >>> Decimal('0.125').quantize(Decimal('0.00'))
    Decimal('0.12')
    >>> Decimal('0.375').quantize(Decimal('0.00'))
    Decimal('0.38')
    

    怎么结果和round一样?我们来看看文档中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.375').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('0.38')
    >>> Decimal('0.125').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('0.13')
    

    现在看起来一切都正常了。

    那么会不会有人进一步追问一下,如果 Decimal 接收的参数不是字符串,而是浮点数会怎么样呢?

    来实验一下:

    
    >>> Decimal(0.375).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('0.38')
    >>> Decimal(0.125).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('0.13')
    

    那是不是说明,在 Decimal 的第一个参数,可以直接传浮点数呢?

    我们换一个数来测试一下:

    >>> Decimal(11.245).quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('11.24')
    >>> Decimal('11.245').quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    Decimal('11.25')
    

    为什么浮点数11.245和字符串'11.245',传进去以后,结果不一样?

    我们继续在文档在寻找答案。

    官方文档已经很清楚地说明了,如果你传入的参数为浮点数,并且这个浮点值在计算机里面不能被精确存储,那么它会先被转换为一个不精确的二进制值,然后再把这个不精确的二进制值转换为等效的十进制值

    对于不能精确表示的小数,当你传入的时候,Python 在拿到这个数前,这个数就已经被转成了一个不精确的数了。所以你虽然参数传入的是11.245,但是 Python 拿到的实际上是11.244999999999...

    但是如果你传入的是字符串'11.245',那么 Python 拿到它的时候,就能知道这是11.245,不会提前被转换为一个不精确的值,所以,建议给Decimal的第一个参数传入字符串型的浮点数,而不是直接写浮点数。

    总结,如果想实现精确的四舍五入,代码应该这样写:

    from decimal import Decimal, ROUND_HALF_UP
    
    origin_num = Decimal('11.245')
    answer_num = origin_num.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP)
    print(answer_num)
    

    运行效果如下图所示:

    特别注意,一旦要做精确计算,那么就不应该再单独使用浮点数,而是应该总是使用Decimal('浮点数')。否则,当你赋值的时候,精度已经被丢失了,建议全程使用 Decimal 举例:

    a = Decimal('0.1')
    b = Decimal('0.2')
    c = a + b
    print(c)
    

    最后,发在 V2EX 上面的内容有删减,完整的内容可以在我的微信公众号里面找到。

    第 1 条附言  ·  2019-03-31 16:15:26 +08:00
    公众号:未闻 Code ( ID:itskingname )

    ![]( https://kingname-1257411235.cos.ap-chengdu.myqcloud.com/wechatplatform.jpg)
    第 2 条附言  ·  2019-03-31 16:15:54 +08:00

    补上二维码

    30 条回复    2019-04-12 14:51:08 +08:00
    grimpil
        1
    grimpil  
       2019-03-31 14:35:53 +08:00 via Android   ❤️ 2
    多谢科普
    inhzus
        2
    inhzus  
       2019-03-31 14:41:48 +08:00 via Android
    印象中操作系统课程写浮点数的运算遇到“四舍五入”的时候,就是要像楼主说的那样判断奇偶。感谢分享
    itskingname
        3
    itskingname  
    OP
       2019-03-31 15:27:00 +08:00   ❤️ 1
    @inhzus Python 3 使用了这种奇进偶舍的办法,但是 Python 2 就非常粗暴。
    aijam
        4
    aijam  
       2019-03-31 15:53:38 +08:00   ❤️ 1
    感觉说复杂了,其实既可以进也可以舍的时候,就一个原则:选偶数返回。round(0.5) = 0, round(1.5) = 2, round(2.5) = 2, round(3.5) = 4, round(4.5) = 4 ...
    chinvo
        5
    chinvo  
       2019-03-31 15:56:06 +08:00 via iPhone
    IEEE 754
    itskingname
        6
    itskingname  
    OP
       2019-03-31 16:00:26 +08:00
    @aijam 你举得这个例子不恰当。因为你的例子是精确到各位了。如果是精确到小数点后 2 位,你的例子并不能说明问题,因为『选偶数返回』适用于整数。但是小数是不存在奇偶性的,那么这个所谓的『选偶数返回』选的是哪个偶数呢?
    bumz
        7
    bumz  
       2019-03-31 16:03:21 +08:00
    @itskingname #6 选择最后一位是偶数的返回
    这里说的都是位,所以省略定语也是无歧义的
    sutra
        8
    sutra  
       2019-03-31 16:04:03 +08:00
    Banker's rounding
    DiamondbacK
        9
    DiamondbacK  
       2019-03-31 16:07:30 +08:00
    ballshapesdsd
        10
    ballshapesdsd  
       2019-03-31 16:09:17 +08:00
    公众号是不是忘说了
    itskingname
        11
    itskingname  
    OP
       2019-03-31 16:10:13 +08:00
    @bumz 实际上就是奇进偶舍
    itskingname
        12
    itskingname  
    OP
       2019-03-31 16:10:38 +08:00
    @ballshapesdsd 末尾不是有二维码吗
    ballshapesdsd
        13
    ballshapesdsd  
       2019-03-31 16:13:11 +08:00
    @itskingname #12 没有看到
    aijam
        14
    aijam  
       2019-03-31 16:15:18 +08:00
    @itskingname 就是 round 结果的最低那一位是偶数。
    itskingname
        15
    itskingname  
    OP
       2019-03-31 16:16:23 +08:00
    @ballshapesdsd 补上到 append 中了。
    sorra
        16
    sorra  
       2019-03-31 18:09:36 +08:00
    很好的文章!
    itskingname
        17
    itskingname  
    OP
       2019-03-31 20:02:43 +08:00 via iPhone
    @sorra 感谢
    mrchi
        18
    mrchi  
       2019-03-31 20:47:30 +08:00
    我看到过掘金上的推送,怎么把吐槽垃圾博客那部分删掉了啊
    itskingname
        19
    itskingname  
    OP
       2019-03-31 22:29:13 +08:00 via iPhone
    @mrchi 以前我在 v 站吐槽过,被站长封号了。所以现在不在 v 站发技术无关的内容了。
    XiaoXiaoNiWa
        20
    XiaoXiaoNiWa  
       2019-04-01 08:46:32 +08:00 via Android
    谢谢分享
    itskingname
        21
    itskingname  
    OP
       2019-04-01 08:50:35 +08:00 via iPhone
    @XiaoXiaoNiWa 有帮助就好
    mrchi
        22
    mrchi  
       2019-04-01 09:33:47 +08:00
    @itskingname 我不只想吐槽那些垃圾博客,还有那些大采集站,腾讯云和阿里云栖社区,权重那么高,内容那么烂。
    itskingname
        23
    itskingname  
    OP
       2019-04-01 10:32:41 +08:00
    @mrchi 确实是这样的。
    bakabie
        24
    bakabie  
       2019-04-01 12:43:36 +08:00
    学到了,感谢科普
    Dvel
        25
    Dvel  
       2019-04-01 16:27:39 +08:00
    涨姿势了!
    itskingname
        26
    itskingname  
    OP
       2019-04-01 17:06:16 +08:00
    @bakabie
    @Dvel 如果对你们有帮助,还请关注公众号并转发,感谢~
    classyk
        27
    classyk  
       2019-04-01 21:55:11 +08:00 via iPhone
    我们说的四舍五入只是 round 的简化版,并非严谨的。请参考 IEEE754 standard
    jiejiss
        28
    jiejiss  
       2019-04-03 09:55:15 +08:00
    @mrchi 在百度搜索的时候加上 -yq.aliyun 即可,然后封装成油猴脚本
    just1
        29
    just1  
       2019-04-08 20:33:24 +08:00
    你好,但是 round(0.35,1)为 0.3 又是为什么呢,按照规则不是应该成双,0.4 吗
    itskingname
        30
    itskingname  
    OP
       2019-04-12 14:51:08 +08:00   ❤️ 1
    @just1 0.35 在 Python 中会变成 0.34999999999999997779553950749686919152736663818359375,此时还轮不到五成双。 你注意我在『如何正确进行四舍五入』这个小节上面的两段话。你这属于第一种情况。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1323 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 17:42 · PVG 01:42 · LAX 09:42 · JFK 12:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.