为什么在Python中,对两个可整除的浮点数进行 math.floor(x/y) 不等于 x // y ?

23

我一直在阅读关于Python中除法和整数除法以及Python2与Python3之间差异的内容。大部分都很容易理解。当两个值都是整数时,Python 2仅使用整数除法,而Python 3总是执行真实的除法。 Python 2.2+引入了用于整数除法的//运算符。

其他程序员提供的示例非常好,例如:

>>> 1.0 // 2.0      # floors result, returns float
0.0
>>> -1 // 2         # negatives are still floored
-1

如何实现//?为什么会发生以下情况:

>>> import math
>>> x = 0.5 
>>> y = 0.1
>>> x / y
5.0
>>> math.floor(x/y)
5.0
>>> x // y
4.0

为什么不写成 x // y = math.floor(x/y) 呢?这些结果是在 Python 2.7 上生成的,但由于 x 和 y 都是浮点数,在 Python 3+ 上结果应该是相同的。如果存在某种浮点误差,使得 x/y 实际上是 4.999999999999999,并且 math.floor(4.999999999999999) == 4.0,那么这不会反映在 x/y 吗?

然而,以下类似情况不受影响:

>>> (.5*10) // (.1*10)
5.0
>>> .1 // .1
1.0
4个回答

24

我并未找到其他满意的答案。当然,.1 没有有限的二进制展开,所以我们的直觉是表示误差是罪魁祸首。但仅凭这一点并不能真正解释为什么 math.floor(.5/.1) 得出 5.0.5 // .1 得出 4.0

关键在于,a // b 实际上是在执行 floor((a - (a % b))/b),而不仅仅是 floor(a/b)

.5 / .1 等于 确切的 5.0

首先要注意的是,在 Python 中,.5 / .1 的结果是 确切地 5.0。即使 .1 不能被精确地表示。例如,考虑以下代码:

from decimal import Decimal

num = Decimal(.5)
den = Decimal(.1)
res = Decimal(.5/.1)

print('num: ', num)
print('den: ', den)
print('res: ', res)

相应的输出为:

num:  0.5
den:  0.1000000000000000055511151231257827021181583404541015625
res:  5
这表明.5可以用有限的二进制展开表示,但.1不能。但这也表明,尽管如此,.5 / .1的结果确实是5.0。这是因为浮点数除法会导致精度丢失,而den.1之间的差异在此过程中丢失。
这就是为什么math.floor(.5 / .1)能够按照您的期望工作的原因:由于.5 / .1 确实是5.0,所以编写math.floor(.5 / .1)与编写math.floor(5.0)是一样的。
那么为什么.5 // .1的结果不是5呢?
人们可能会认为.5 // .1floor(.5 / .1)的简写,但事实并非如此。尽管PEP说:

整数除法将在所有Python数值类型中实现,并且具有以下语义:

    a // b == floor(a/b)
事实证明,.5 // .1 的语义实际上等同于:
floor((.5 - mod(.5, .1)) / .1)

其中mod.5 / .1的浮点余数向零舍入。通过阅读Python源代码可以清楚地看到这一点。

这里出现问题的原因是.1在二进制展开中无法精确表示。 因此,.5 / .1的浮点余数不是零:

>>> .5 % .1
0.09999999999999998

由于十进制数.1的二进制展开略大于实际值,因此在我们有限精度的计算中,最大的整数alpha满足alpha * .1 <= .5alpha = 4。因此mod(.5, .1)不为零,大约为.1。因此floor((.5 - mod(.5, .1)) / .1)变成了floor((.5 - .1) / .1),即floor(.4 / .1),等于4

这就是为什么.5 // .1 == 4的原因。

为什么//会这样做?

a // b的行为可能看起来很奇怪,但是它与math.floor(a/b)的差异是有原因的。在他关于Python历史的博客中,Guido写道:

 

整数除法(//)及其兄弟模运算(%)一起使用并满足一个很好的数学关系(所有变量都是整数):

a/b = q with remainder r

如此,以致于

b*q + r = a and 0 <= r < b

(假设a和b都大于等于0)。

现在,Guido假定所有变量都是整数,但如果q = a // b,这个关系仍然成立,即使ab是浮点数也一样。如果q = math.floor(a/b),通常情况下这个关系不成立。因此,//可能更受欢迎,因为它满足这种美妙的数学关系。


考虑到与PEP的矛盾,您认为这是否算作解释器中的一个bug? - zwol
1
@zwol,我认为这不是解释器的错误,而是PEP的错误。有理由更喜欢a // b而不是math.floor(a, b),我看不到文档承认这一点,而PEP关于//语义的陈述似乎明显是不正确的。如果您感兴趣,我已在答案中添加了相关说明。 - jme
1
.5 // .1 不等同于 floor((.5 - mod(.5, .1)) / .1);当你阅读源代码时,你可能会错过 floordiv = floor(div); 后面直接跟着 if (div - floordiv > 0.5) floordiv += 1.0; 的部分。这意味着它进行四舍五入而不是向下取整。 - user2357112
1
还需要注意的是,4.0正确舍入的结果 - 也就是说,如果你取两个浮点数输入.5.1所代表的实数(而不是实际的数字.5和.1),应用理想的数学函数f(x, y) = floor(x/y)而没有中间舍入,然后选择最接近实数结果的浮点数作为返回值,那么你会得到4.0。我还没有分析过floordiv实现是否比应用浮点除法和浮点floor更频繁地产生正确舍入的结果... - user2357112
按顺序,无论如何,保留“//”和“%”之间的关系可能更为重要。 - user2357112
是的,我省略了这个部分,因为在 .5 / .1 的特定情况下,if 语句的四舍五入分支不会被执行,但你说得对,通常结果是四舍五入而不是向下取整。我会更新我的答案以澄清这一点。谢谢! - jme

6
那是因为
>>> .1
0.10000000000000001

.1无法在二进制中精确表示。

您还可以看到

>>> .5 / 0.10000000000000001
5.0

1
@J.Money:尝试使用(0.1).as_integer_ratio()format(0.1, '1.30f') - DSM
1
@searchengine27 “.1 无法在二进制中精确表示” 这不足以解释吗?(此外,其他答案提供了不同程度的细节,user5248483发布了一个链接到 Python 3 教程上关于浮点数的所有微妙之处的很好的解释) - njzk2
1
根本不行。浮点数的公式在哪里?事物实际上是如何表示的解释呢?说“那不起作用”并不等同于说“它是如何工作的”。参考IEEE 754可能是一个开始,但你甚至没有努力去做到这一点。 - searchengine27
3
说别人发了正确的答案,并不会让你的回答变得正确。这只能证明他们的答案正确,而且更加突显出你的答案是不完整的。看,你可以在评论中坐在这里哭泣,因为我指出了你的错误并且你明显很生气,或者接受我提供给你的机会来修正你的答案。另外,他的答案也不完整,因为这不是一个仅限于Python的问题。它影响任何基于2进制的CPU(我不知道有非基于2进制的CPU)。 - searchengine27
1
@searchengine27 请投票选出您认为最好回答了问题的答案(如果没有,请考虑自己提供一个答案)。“被采纳的答案”纯粹是提问者的选择,无论您对主题有何看法,“这就是它的工作方式”(至于原因和解释,我建议您去Meta了解更多信息)。 - njzk2
显示剩余6条评论

2
问题在于Python会将输出四舍五入,如这里所述。由于二进制无法精确表示0.1,结果会变成类似于4.999999999999999722444243844000的值。因此,在不使用格式化时,自然而然地会变成5.0

1
优秀的资源。那正是我在寻找的文档。 - None
1
这不正确,恐怕是。.5 / .1 恰好等于 5.0。参见:(.5/.1).as_integer_ratio() ,其结果为 (5,1) - jme

-1
抱歉,这是不正确的。.5 / .1确切地等于5.0。请参见:(.5/.1).as_integer_ratio(),其产生(5,1)。
是的,5可以表示为5/1,没错。但是要查看Python给出的由于不精确表示而产生的实际结果的分数,请跟着做。
首先,导入:
from decimal import *
from fractions import Fraction

方便使用的变量:

// as_integer_ratio() returns a tuple
xa = Decimal((.5).as_integer_ratio()[0])
xb = Decimal((.5).as_integer_ratio()[1])
ya = Decimal((.1).as_integer_ratio()[0])
yb = Decimal((.1).as_integer_ratio()[1])

返回以下值:

xa = 1
xb = 2
ya = 3602879701896397
yb = 36028797018963968

自然地,1/2 == 5,且 3602879701896397 / 36028797018963968 == 0.1000000000000000055511151231
那么当我们进行除法运算时会发生什么?
>>> print (xa/xb)/(ya/yb)
4.999999999999999722444243845

但是当我们想要整数比率时...

>>> print float((xa/xb)/(ya/yb)).as_integer_ratio()
(5, 1)

如前所述,5 当然是 5/1。这就是 Fraction 的用处:

>>> print Fraction((xa/xb)/(ya/yb))
999999999999999944488848769/200000000000000000000000000

wolfram alpha证实这确实是4.999999999999999722444243845


为什么不直接使用Fraction(.5/.1)或者Fraction(Decimal(.5)/Decimal(.1))呢?

后者会给我们相同的结果5/1。前者将会得到1249999999999999930611060961/250000000000000000000000000,这将导致4.999999999999999722444243844,虽然结果相似但并非完全相同。


2
在这里你必须小心:xaxbyaybDecimal对象,而不是Python的float,所以展示(xa/xb)/(ya/yb)4.999...仅仅证明了Decimal类提供的任意精度算术。如果你写Decimal(.5 / .1),你会看到.5 / .1恰好是5.0——即使.1存在表示误差,但在除法中误差被消除了。 - jme

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