大家好,欢迎来到 Crossin的编程教室~
「四舍五入」是小学就学过的数学知识,也是日常计算中经常会用到的处理方法。
然而让人没想到的是,一个简单的四舍五入操作,在Python里居然这么难搞,网上还一堆错误的教程。
来看这个例子,有一个变量a为1.135,现在希望把它保留2位小数,要怎么做?
网上搜索一下,找到两种方法:
第1种,round函数。第一个参数是原数字,第二个参数是要保留的小数位数
round(a, 2)
结果 1.14,没有问题。
第2种,通过格式说明符.f对浮点数进行字符串格式化,f前加上要保留的小数位数。
这种写法在 %格式化、format方法和f-string 上均适用。
print('%.2f' % a, '{:.2f}'.format(a), f'{a:.2f}')
结果 1.14,也没有问题。
所以看来,以上两种方法都可以实现四舍五入地保留小数位数……
but,真的是这样吗?
显然事情没这么简单。如果把a的值改成1.125,再跑一下之前的代码,就发现两种方法都不对了。
别小看这么一点误差,我曾经在做助教时,就因为类似的原因,导致一位原本算好被60分放过的同学挂了科。
有些不靠谱的半瓶水教程会跟你说,这是因为Python用了种叫做「四舍六入五成双」的保留机制:5前面的数字是奇数就进位,是偶数就保持不变,所以1.135会得到1.14,而1.125就是1.12。
还有的教程告诉你有种方法可以实现四舍五入:就是把要保留N位的小数,乘以10的N次方,加上0.5后取整,再除以10的N次方
int(a * 10 ** 2 + 0.5) / 10 ** 2)
结果确实是 1.13。
但你以为这样就可以了吗?
如果再把a改成1.035。结果既没有什么所谓的奇数进位,也没能通过先乘再除的方法实现四舍五入。
把1.005,1.015,1.025,一直到1.995,用前面提到过的3种方法保留2位小数的结果输出出来就会发现。round和字符串格式化得到的保留结果是一样的,且基本没有规律可言。
而先乘后除法虽然在大部分情况下是符合四舍五入的,但仍然有一些例外的情况。
导致这种现象的原因,和之前讲解过的 0.1 + 0.2 != 0.3一样,都是因为浮点数的精度造成的。
让这些小数输出更多位数,就会看到,很多值虽然结尾是5,但在计算机中以二进制存储的实际值其实不到5。那么按照四舍五入来说,当然是要被舍去了。
真正可以做到对小数保留位数进行精确控制的方法是使用 Python 内置的 decimal 模块,它用于高精度的十进制算术运算。
用 round 函数对于 Decimal 类型对象进行保留,才是真正的四舍六入五成双。
from decimal import Decimalx = 1.035print(round(Decimal(str(x)), 2))
这种机制又被称作「银行家舍入」,它其实比四舍五入更合理。因为5是两个数的中间值,全都进位会让数据在整体分布上偏大,而银行家舍入规则可以让累积误差趋向于0。
如果你就是想要按照四舍五入来保留,也可以,通过将 Context 里的 rounding 属性设置为 ROUND_HALF_UP 就可以
from decimal import Decimal, ROUND_HALF_UP, getcontextx = 1.045getcontext().rounding = ROUND_HALF_UPprint(round(Decimal(str(x)), 2))
另一种写法是通过 Decimal 的 quantize 方法,指定保留位数和舍入规则,效果是一样的。
from decimal import Decimalx = 1.045print(Decimal(str(x)).quantize(Decimal('0.01'), rounding='ROUND_HALF_UP'))
这样,就能完美地按四舍五入保留小数了。
不过这里还有一个小小的坑,就是一定要通过字符串去创建 Decimal 对象,否则实际值仍然是带有误差的,从而导致四舍五入失效。
好吧,没想到一个简单的四舍五入操作,竟然还这么复杂,你学会了吗?
作者:Crossin的编程教室
Crossin的新书《码上行动:用ChatGPT学会Python编程》已经上市了。本书以ChatGPT为辅助,系统全面地讲解了如何掌握Python编程,适合Python零基础入门的读者学习。【点此查看详细介绍】
领取专属 10元无门槛券
私享最新 技术干货