我们来说说,在 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
,需要精确到小数点后两位,那么就要看小数点后第三位:
d
小于 5,直接舍去d
大于 5,直接进位d
等于 5:
d
后面没有数据,且 c 为偶数
,那么不进位,保留 cd
后面没有数据,且 c 为奇数
,那么进位,c 变成(c + 1)d
后面还有非 0 数字,例如实际上小数为a.bcdef
,此时一定要进位,c 变成(c + 1)关于奇进偶舍,有兴趣的同学可以在维基百科搜索这两个词条:数值修约
和奇进偶舍
。
所以,round
给出的结果如果与你设想的不一样,那么你需要考虑两个原因:
1.115
,它的小数点后第三位实际上是4
,当然会被舍去。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.125
和0.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 上面的内容有删减,完整的内容可以在我的微信公众号里面找到。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.