在 Python 中如何正确地四舍五入?
用 Python 进行四舍五入是一个很常见的需求。但是经常有人用习惯性的思维去理解,导致出现与期望不同的现象,进而引发各种问题。
以下均以保留 2 位小数为例
round 函数的问题
这是网上的普遍的用法:
1 |
|
可见,round 并不总是能返回我们想要的结果的。
一个容易发现的原因就是十进制小数转二进制时精度丢失的问题,即 Python 存储 1.115
的时候实际上是 1.1149...
:
1 |
|
而对 1.1149
保留2位小数,当然是 1.11
而不是 1.12
了。
但是,十进制小数转二进制的时候也会出现能完全转换的,例如:1.125
:
1 |
|
那么这时 round 的结果会符合我们的预期吗?
1 |
|
依然不行。这又是什么原因呢?
实际上,计算机语言都是这样处理的,这个方式叫:奇进偶舍
:
> 例如数 a.bcd,我们需要保留 2 位小数的话,要看小数点后第三位:
- 如果 d<5,直接舍去
- 如果 d<5,直接进位
- 如果 d == 5:
- d 后面还有非 0 数字,例如 a.bcdef,f 非 0,那么要进位
- d 后面没有数据,且 c 为偶数,那么不进位
- d 后面没有数据,且 c 为奇数,那么要进位
所以,我们可以总结一下,假如你要使用 round 来保留位数,那么你需要考虑 2 个问题:
- 这个数可能无法被计算机精确地表示,这个误差是否能接受?
- 奇进偶舍这个方法的误差是否能接受?
如果有一个答案是:不能,你就需要 decimal(其他的方法不考虑,见文章最后)。
decimal: 高精度解决方案
由我们自己去关注上面提到的问题显然是反人类的。于是 Python 提供了用于处理高精度数字的库:decimal
。
这个库直接解决了上面的第一个问题。
但是使用的时候需要注意给 Decimal
传字符串与浮点数的区别:
1 |
|
为什么两种不同呢?原因在于直接传浮点的时候,Python 没法存下 1.115
,将 1.115
转为 1.11499...
,所以在给 Decimal
的时候就已经丢失精度了。所以使用 decimal 的时候最好用字符串传参。并且,一定要全程使用 Decimal
类型,不要混用 float 与 Decimal
类型
接下来我们对 1.115
试试四舍五入,看看是不是我们需要的:
1 |
|
不错,是我们需要的。再试试 0.125
:
1 |
|
居然还是不行。问题又出在哪呢?其实和之前的 round
一样,也是 奇进偶舍
。为了证明这一点,我们可以去 Python 的官方文档查阅 quantize
的用法:
quantize
可以看到:
简单的翻译一下就是:quantize
有个可选参数 rounding
。如果不指定它的话,使用的是 context
的值。如果 context
也没指定值,那就用此线程的上下文中的 rounding
。
那么问题来了,rounding
到底有哪些值可选呢?文档也有:
那么我们显然没指定 rounding
,上下文用的是哪个呢?我们打印一下看看:
1 |
|
ROUND_HALF_EVEN
即 Round to nearest with ties going to nearest even integer.
,就是我们上面提到的 奇进偶舍
。
而 ROUND_HALF_UP
才是我们想要的:
1 |
|
这下第二个问题也解决了。
后记
感慨于这么简单的需求实际上有很多东西蕴含在里面。Python 尚且如此,其他语言呢?我特意问了一下擅长 C++ 的 Barriery,他说 C++里是这样四舍五入的:
1 |
|
2 位就是 100,3 位就是 1000,以此类推。我拿到 Python 里试了一下,还真的可以:
1 |
|
原理嘛,就是利用了 int 直接截断的特性。不得不说思路真是无奇不有啊。不过我还是推荐 decimal 这个库,专门处理高精度的数字,相当好用。
- By:tr0y.wang
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论