Python浮点数取模

57
有人能解释一下 Python 中模运算符是如何工作的吗?我不明白为什么 3.5 % 0.1 = 0.1

可能是重复的问题:浮点数运算是否存在问题? - user202729
1
@user202729,确实接受的答案包含了与您提出的建议相关的信息,但同时它也具有一些Python特定的内容,因为这是一个关于模运算的Python问题。 - beruic
3个回答

101

实际上,3.5 % 0.1 不等于 0.1。您可以很容易地测试一下:

>>> print(3.5 % 0.1)
0.1
>>> print(3.5 % 0.1 == 0.1)
False

事实上,在大多数系统中,3.5 % 0.1的结果是0.099999999999999811。但是,在某些Python版本中,str(0.099999999999999811)的结果为0.1

>>> 3.5 % 0.1
0.099999999999999811
>>> repr(3.5 % 0.1)
'0.099999999999999811'
>>> str(3.5 % 0.1)
'0.1'
现在,您可能会想知道为什么3.5%0.1的结果是0.099999999999999811而不是0.0。那是因为通常存在浮点舍入问题。如果您还没有阅读过《计算机科学家应该了解的浮点算术》,则应该阅读此文或至少查看维基百科上对此特定问题的简要说明。
请注意,3.5 / 0.1并非34,而是35。 因此,3.5 / 0.1 * 0.1 + 3.5%0.1等于3.5999999999999996,这甚至与3.5相差甚远。 这几乎是模数定义中的基本问题,在Python和几乎所有其他编程语言中都是错误的。
但是,Python 3在这方面提供了帮助。 大多数了解//的人知道这是在整数之间进行“整数除法”的方法,但不知道它也适用于任何类型之间的模数兼容除法。 3.5 // 0.1等于34.0,因此3.5 // 0.1 * 0.1 + 3.5%0.1(至少在小数值上)等于3.5。 这已经回溯到2.x,因此(取决于您的确切版本和平台)您可能可以依赖此功能。 如果不能,则可以使用divmod(3.5,0.1),它会返回(在舍入误差内)(34.0, 0.09999999999999981)一直追溯到最初的版本。 当然,您仍然期望这将是(35.0, 0.0),而不是(34.0,几乎为-0.1),但由于舍入误差而无法实现。
如果您正在寻找快速解决方案,请考虑使用Decimal类型。
>>> from decimal import Decimal
>>> Decimal('3.5') % Decimal('0.1')
Decimal('0.0')
>>> print(Decimal('3.5') % Decimal('0.1'))
0.0
>>> (Decimal(7)/2) % (Decimal(1)/10)
Decimal('0.0')

这不是一个神奇的万灵药 - 例如,当在十进制中无法有限地表示操作的精确值时,您仍然必须处理舍入误差。但是,与 float 相比,舍入误差更符合人类直觉会出问题的情况。( Decimal 还具有其他优点,例如可以指定明确的精度、跟踪有效数字等,并且在从2.4到3.3的所有Python版本中都完全相同,而有关 float 的细节在同一时间内已经改变了两次。只是它并不完美,因为那是不可能的。)但是,当您事先知道您的所有数字都可以在十进制中精确表示,并且它们不需要比您配置的精度更多的位数时,它将起作用。


1
你忽略了一个最重要的事实,即 0.1 会创建一个比 0.1 稍微大一点的浮点数。因此,35 % 0.100000... = 0.9999999...。你的帖子从未提到过这一点,而总是假设 0.1 实际上是 0.9999...。但是,正如我的答案所示,情况并非如此。 - bikeshedder
1
3.5 / 0.1000000... = 34.99999....,但由于舍入误差,最终结果为35。因此,看起来3.5 / 0.1给出了精确的结果35。实际上,这里有两个相互抵消的舍入误差。请参见http://ideone.com/fTNVho,该网站非常好地展示了这种行为。 - bikeshedder
你还在纠结于无关紧要的事情。请参见http://ideone.com/zISIdx。3.5/0.1非常接近35.0,而从0.1向任何方向移动一小段距离都不会改变这一点。但是3.5%0.1就不同了——向一个方向移动一小段距离几乎为0,在另一个方向移动则几乎为0.1。这是关键问题。这与一个数字足够接近以至于可以精确打印出来即使它并不精确,或者舍入误差抵消掉没有任何关系。同时,你仍然没有回答我的问题。我的答案假设`0.1`实际上是`0.09999...`在哪里? - abarnert
10
不,我对继续这个讨论没有兴趣。其他人都理解了我的回答并认为它回答了问题。您添加的恶意投票和一大堆无意义的话并不重要。该网站旨在提供有用的答案,而不是最大化声望点数或说服恶意评论者。 - abarnert
7
我现在打算继续使用整数。 - beruic
显示剩余8条评论

13

模运算可以给你除法的余数。例如,3.5 除以 0.1 应该得到商为 35,余数为 0。但是由于浮点数是基于二进制幂的,所以这些数字并不是 准确的,因此会出现舍入误差。

如果你需要精确计算十进制数的除法,请使用 decimal 模块:

>>> from decimal import Decimal
>>> Decimal('3.5') / Decimal('0.1')
Decimal('35')
>>> Decimal('3.5') % Decimal('0.1')
Decimal('0.0')

由于我的回答误导了人们,下面是整个故事:

Python浮点类型0.1略大于十分之一:

>>> '%.50f' % 0.1
'0.10000000000000000555111512312578270211815834045410'

如果你用某个数字去除浮点数3.5,你会得到一个接近0.1的余数。

让我们从数字0.11开始,然后在两个1之间继续添加零,以使其变小,同时保持大于0.1

>>> '%.10f' % (3.5 % 0.101)
'0.0660000000'
>>> '%.10f' % (3.5 % 0.1001)
'0.0966000000'
>>> '%.10f' % (3.5 % 0.10001)
'0.0996600000'
>>> '%.10f' % (3.5 % 0.100001)
'0.0999660000'
>>> '%.10f' % (3.5 % 0.1000001)
'0.0999966000'
>>> '%.10f' % (3.5 % 0.10000001)
'0.0999996600'
>>> '%.10f' % (3.5 % 0.100000001)
'0.0999999660'
>>> '%.10f' % (3.5 % 0.1000000001)
'0.0999999966'
>>> '%.10f' % (3.5 % 0.10000000001)
'0.0999999997'
>>> '%.10f' % (3.5 % 0.100000000001)
'0.1000000000'

最后一行给人的印象是我们终于到达了0.1,但更改格式字符串揭示了真正的性质:
>>> '%.20f' % (3.5 % 0.100000000001)
'0.09999999996600009156'

Python的默认浮点数格式并不能显示足够的精度,所以3.5 % 0.1 = 0.13.5 / 0.1 = 35.0是错误的。实际上,3.5 % 0.100000... = 0.999999...3.5 / 0.100000... = 34.999999....。在除法的情况下,你最终得到了一个准确的结果,因为34.9999...会被最终四舍五入成35.0


有趣的是:如果你使用一个略小于0.1的数字并执行相同的操作,你最终得到的数字将稍微大于0

>>> 1.0 - 0.9
0.09999999999999998
>>> 35.0 % (1.0 - 0.9)
7.771561172376096e-15
>>> '%.20f' % (35.0 % (1.0 - 0.9))
'0.00000000000000777156'

使用C++,甚至可以证明浮点数“3.5”除以浮点数“0.1”不是“35”,而是略小于“35”。
#include <iostream>
#include <iomanip>

int main(int argc, char *argv[]) {
    // double/float, rounding errors do not cancel out
    std::cout << "double/float: " << std::setprecision(20) << 3.5 / 0.1f << std::endl;
    // double/double, rounding errors cancel out
    std::cout << "double/double: " << std::setprecision(20) << 3.5 / 0.1 << std::endl;
    return 0;
}

http://ideone.com/fTNVho

在Python中,3.5 / 0.1会给出精确的结果35,因为舍入误差会相互抵消。实际上它是3.5/0.100000...,而这个数字太长了,最终你得到的结果就是35。C++程序可以很好地展示这一点,因为您可以混合使用double和float,并调整浮点数的精度。


但在Python(以及大多数其他语言)中,35.0除以0.1会给你35,但它却给你一个余数为0.1,这与模运算的定义不兼容。 - abarnert
这一切都与舍入有关。使用浮点数时,不存在所谓的“0.1”。由于其性质(请参见:IEEE浮点),十进制数字“0.1”无法转换为精确浮点数。你最终得到的是像“0.099999999999999811”或“0.10000000000000000555……”这样的东西。如果你想检查数字,请不要使用str(0.1),而应该使用类似"%.100f" % 0.1这样的东西。 - bikeshedder
1
我理解四舍五入的概念(我在你发表回答前已经解释过了15分钟)。但这还不是全部。0.1并非只是因为一个小的四舍五入误差而与0.0有所不同(同样地,35.0也不仅仅是因为一个小的四舍五入误差而与34.0有所不同)。通常的解决方案“只需加入适当的epsilon”并不能解决问题;对于任何合理的epsilon值,abs(0.1-0.0) < eps都不会成立。所以,你的答案是误导性的。 - abarnert
@abarnert 这个问题是“为什么35%0.1 = 0.1”,这就是我的答案所涉及的内容。我更新了我的答案,以便更深入地解释发生了什么以及为什么会得到那些奇怪的结果。希望这样能更好地解释清楚。 - bikeshedder
你最新添加的代码 http://ideone.com/fTNVho 也是误导性的。它并没有展示“你可以混合使用double和float并玩转精度”的内容。它只是展示了 (double)0.1f != 0.1(参见http://ideone.com/lFgkd9)。当你对一个double和一个float进行算术运算时,C++会将float扩展为double,然后执行双倍精度算术运算。 - abarnert
显示剩余2条评论

2

这与浮点数运算的不精确性有关。 3.5 % 0.1 得到的结果是 0.099999999999999811,因此Python认为0.1最多能被34整除,剩余0.099999999999999811。我不确定用于得出这个结果的算法是什么,但这就是要点。


这几乎是正确的,除了它暗示着 3.5 / 0.134.0,实际上不是,应该是 35.0。(另外,它也没有解释为什么他看到的是 0.1 而不是 0.099999999999999811。) - abarnert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接