Python floor division 中的舍入错误

rounding errors in Python floor division

提问人:0x539 提问时间:7/26/2016 最后编辑:wjandrea0x539 更新时间:7/27/2023 访问量:10737

问:

我知道在浮点运算中会发生舍入错误,但有人可以解释一下原因吗:

>>> 8.0 / 0.4  # as expected
20.0
>>> floor(8.0 / 0.4)  # int works too
20
>>> 8.0 // 0.4  # expecting 20.0
19.0

这发生在 x64 上的 Python 2 和 3 上。

在我看来,这要么是一个错误,要么是一个非常愚蠢的规范,因为我看不出最后一个表达式应该计算为 .//19.0

为什么不简单地定义为 ?a // bfloor(a / b)

EDIT: 的计算结果也为 .至少这是结果,因为那时评估为8.0 % 0.40.39999999999999968.0 // 0.4 * 0.4 + 8.0 % 0.48.0

编辑:这不是浮点数学是否损坏的重复? 因为我在问为什么这个特定的操作会受到(也许是可以避免的)舍入误差的影响,以及为什么不定义为 / 等于a // bfloor(a / b)

备注:我猜这不起作用的更深层次原因是楼层划分是不连续的,因此具有无限的条件数,使其成为一个不合理的问题。楼层除法和浮点数从根本上是不兼容的,您永远不应该在浮点数上使用。只需改用整数或分数即可。//

Python 浮点 舍入

评论

4赞 khelwood 7/26/2016
有趣的是,给 ,所以显然只是有点过分了。'%.20f'%0.4'0.40000000000000002220'0.40.4
2赞 Aswin Murugesh 7/26/2016
@khelwood如何产生正确的结果?floor(8.0/0.4)
2赞 TigerhawkT3 7/26/2016
首先,具有该类型的浮点数通常是错误的。其次,对于负数和数字来说,它们非常不可靠(意味着,意外的行为)。有关 Decimal 对象的文档简要讨论了负整数以及库如何以不同的方式处理负整数。float//%float//Decimal
3赞 Alexander Vogt 7/27/2016
浮点数学是否损坏的可能重复?
4赞 jotasi 7/27/2016
@AlexanderVogt 不是真的,是吗?问题不在于为什么浮点结果不准确,而在于为什么 python 会做两件不同的事情,而在于“地板除法”。floor(8.0/0.4)8.0//0.4

答:

7赞 Mazdak 7/26/2016 #1

那是因为 python(浮点有限表示)中没有 0.4,它实际上是一个浮点数,这使得除法的下限为 19。0.4000000000000001

>>> floor(8//0.4000000000000001)
19.0

但是,如果参数是浮点数或复数,则真正的除法 () 返回除法结果的合理近似值。这就是为什么结果是 20 的原因。它实际上取决于参数的大小(在 C 双参数中)。(未四舍五入到最接近的浮点数/8.0/0.4)

阅读 Guido 本人的 python 整数除法楼层的更多信息。

此外,有关浮点数的完整信息,您可以阅读本文 https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

对于那些有兴趣的人,在 Cpython 的源代码中,以下函数是对浮点数执行真正的除法任务:float_div

float_div(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    if (b == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "float division by zero");
        return NULL;
    }
    PyFPE_START_PROTECT("divide", return 0)
    a = a / b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

最终结果将按函数计算:PyFloat_FromDouble

PyFloat_FromDouble(double fval)
{
    PyFloatObject *op = free_list;
    if (op != NULL) {
        free_list = (PyFloatObject *) Py_TYPE(op);
        numfree--;
    } else {
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    /* Inline PyObject_New */
    (void)PyObject_INIT(op, &PyFloat_Type);
    op->ob_fval = fval;
    return (PyObject *) op;
}

评论

0赞 jotasi 7/26/2016
@Kasramvd感谢您的广泛回答。也许我只是密集,但我不明白,你说的“捕捉到下一个整数值”是什么意思。显然,并非所有浮点除法都会四舍五入到下一个整数值(不会给出)。因此,据我所知,这个决定不可能像你提出的那样简单。我理解正确吗?3./4.1
1赞 jotasi 7/26/2016
实际上,在自己检查了源代码之后,我猜浮点除法是在函数中完成的,而只有通过它进行下限除法才能调用,这反过来又给出了“错误”的结果而不是。float_divfloat_divmodfloat_floor_div1920
0赞 Mazdak 7/27/2016
@jotasi 是的,没错。它比简单的捕捉更复杂。是的,它的功能可以完成真正的潜水任务。似乎它以某种方式根据参数大小计算最终结果。我更新了答案。感谢您的关注。float_div
0赞 jotasi 7/27/2016
我仔细检查了 c 中的重要行,显然重要的部分是它们通过 c 中的简单除法进行计算,而地板除法实际上是由于浮点运算而计算的。有关更多信息,请参阅下面的回答。8.0/0.4 = 20doublefloor((8.0 - fmod(8.0, 0.4)) / 0.4) = 19fmod(8.0, 0.4) = 0.4
3赞 user2357112 7/27/2016
“事实是,这取决于可用的大小”——什么?不,它没有。s 的大小都相同,s 的存储细节与任何这种行为几乎无关。PyFloatObjectsPyFloatObjectPyFloatObject
9赞 0x539 7/26/2016 #2

好的,经过一番研究,我发现了这个问题。 似乎正在发生的事情是,正如@khelwood建议的那样,在内部计算为 ,当除法时,它会产生比 略小的东西。然后,运算符四舍五入到最接近的浮点数,即 ,但运算符立即截断结果,生成 。0.40.400000000000000022208.020.0/20.0//19.0

这应该更快,我想它“接近处理器”,但我仍然不是用户想要/期望的。

评论

7赞 Jongware 7/26/2016
好发现,那个。但是用户在这里想要什么?对一开始就不正确的数字进行正确的数学行为?(其中,同样的普通“典型用户”通常幸福地不知道
1赞 0x539 7/26/2016
@RadLexus 用户希望此操作获得最佳近似值。在这种情况下,即20.0
5赞 7/26/2016
@0x539:那些依靠截断东西的可怜的用户呢?这里的问题是用户想要进行精确的算术运算,并且使用了错误的工具来完成这项工作。//20.019.0
1赞 jotasi 7/26/2016
实际上,截断不是发生的事情,至少如果我正确理解了 cpython 的来源。他们经历了相当大的考验,通过实际计算来保留您的链接中提到的身份,并且错误是由 引入的。(请参阅我的答案以获取链接和更多解释)。floor((8.0 - fmod(8.0, 0.4)) / 0.4)fmod(8.0, 0.4)=0.4
3赞 Kyle Strand 7/27/2016
从数学上讲,你是正确的,“产生比 略小的东西。但是,将浮点运算视为一系列步骤是不正确的,在这些步骤中,实际的实际数学值被计算出来,然后四舍五入(当你说“运算符然后四舍五入...”时,你暗示了这一点)。当然,这是不可能的,因为计算机必须有一种方法可以在内部表示计算的所有中间步骤!请看@jotasi的答案。8.0 / 0.4000000000000000222020.0/
10赞 shiva 7/26/2016 #3

@jotasi解释了其背后的真正原因。

但是,如果你想防止它,你可以使用模块,它基本上被设计为完全表示十进制浮点数,而不是二进制浮点表示。decimal

因此,在您的情况下,您可以执行以下操作:

>>> from decimal import *
>>> Decimal('8.0')//Decimal('0.4')
Decimal('20')

参考: https://docs.python.org/2/library/decimal.html

评论

0赞 Mazdak 7/26/2016
虽然它不是问题的答案,但它也不是正确的用法,因为当我们可以简单地使用真除法来获得这个结果时。decimal
0赞 GingerPlusPlus 7/26/2016
fractions 模块似乎也在做这项工作。
0赞 Kyle Strand 7/27/2016
@0x539的解释实际上并不正确。请参阅jotasi的回答和我在0x539的答案下面的评论。
1赞 das-g 7/27/2016
@KyleStrand这种保留当然也适用于我的回答,因为我对它做了一些修改。
1赞 Kyle Strand 7/27/2016
@shiva 对不起,之前的评论是针对 das-g 编辑到他们自己的答案;你的仍然不正确......
33赞 das-g 7/26/2016 #4

正如您和 khelwood 已经注意到的那样,不能完全表示为浮点数。为什么?它是五分之二 (),没有有限的二进制分数表示。0.44/10 == 2/5

试试这个:

from fractions import Fraction
Fraction('8.0') // Fraction('0.4')
    # or equivalently
    #     Fraction(8, 1) // Fraction(2, 5)
    # or
    #     Fraction('8/1') // Fraction('2/5')
# 20

然而

Fraction('8') // Fraction(0.4)
# 19

在这里,被解释为一个浮点文字(因此是一个浮点二进制数),它需要(二进制)舍入,然后才转换为有理数,它几乎但不完全是 4 / 10。然后执行地板除法,并且因为0.4Fraction(3602879701896397, 9007199254740992)

19 * Fraction(3602879701896397, 9007199254740992) < 8.0

20 * Fraction(3602879701896397, 9007199254740992) > 8.0

结果是 19,而不是 20。

同样的情况可能也发生在

8.0 // 0.4

也就是说,地板除法似乎是原子确定的(但在解释的浮点文字的唯一近似浮点值上)。

那么为什么会这样

floor(8.0 / 0.4)

给出“正确”的结果?因为在那里,两个舍入错误会相互抵消。首先 1) 执行除法,得到略小于 20.0 的东西,但不能表示为浮点数。它被舍入到最接近的浮点数,恰好是 。只有这样,才执行操作,但现在完全作用于 ,因此不再更改数字。20.0floor20.0


1)正如凯尔·斯特兰德(Kyle Strand)所指出的,确定然后四舍五入的确切结果并不是实际发生的低2)级(CPython的C代码甚至CPU指令)。但是,它可以是确定预期 3) 结果的有用模型。

2)然而,在最低的4)级别上,这可能不会太远。一些芯片组通过首先计算更精确(但仍然不准确,只是具有更多二进制数字)内部浮点结果,然后四舍五入到IEEE双精度来确定浮点结果。

3)Python规范“预期”,不一定是我们的直觉。

4)嗯,逻辑门上方的最低级别。我们不必考虑使半导体成为可能的量子力学来理解这一点。

评论

2赞 Kyle Strand 7/27/2016
“看来底除法是原子确定的”——很好的猜测,我想在语义上是正确的,但就实现必须做什么而言,它有点倒退:由于没有支持“原子”语义的硬件支持,余数是预先计算的,并从分子中减去,以确保浮点除法(当它最终发生时)只是立即计算正确的值, 无需进一步调整。//
1赞 das-g 7/27/2016
是的,我在这里使用来自用户(即 Python 程序员)视图的术语“原子”。类似于例如,某些数据库操作可以被描述为“原子”,它们也不映射到单个硬件指令。所以我说的是效果,而不是实施。
0赞 das-g 7/27/2016
当然,硬件是否支持等效于 Python 运算符的本机指令,当然取决于硬件和操作数类型。早期的 CPU 当然对整数操作数有整数除法支持。可能没有任何芯片组原生支持浮子的地板划分,但这也不是不可想象的,因为它只是不切实际,而不是不可能。//
1赞 jotasi 7/27/2016
“同样的情况可能也发生在”上。不是真的,至少对于 cpython 来说是这样。他们实际上宁愿这样做,因为(至少对于我的机器/编译器版本)导致(也在纯 C 中)。有关详细信息,请参阅我的答案。8.0//0.4round((8.0 - fmod(8.0, 0.4)) / 0.4)19fmod(8.0/0.4)0.4
15赞 jotasi 7/26/2016 #5

在 github (https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c) 上检查了 cpython 中浮点对象的半官方源代码后,可以理解这里发生了什么。

对于普通除法称为(第 560 行),它在内部将 python s 转换为 c-s,进行除法,然后将结果转换回 python。如果你只是在 c 中这样做,你会得到:float_divfloatdoubledoublefloat8.0/0.4

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    printf("%lf\n", floor(vx/wx));
    printf("%d\n", (int)(floor(vx/wx)));
}

// gives:
// 20.000000
// 20

对于地板部门,发生了其他事情。在内部,(第 654 行)被调用,然后调用 ,该函数应该返回一个包含地板除法以及 mod/余数的 python s 元组,即使后者只是被 抛弃了。这些值的计算方式如下(转换为 c-s 后):float_floor_divfloat_divmodfloatPyTuple_GET_ITEM(t, 0)double

  1. 余数是使用 计算的。double mod = fmod(numerator, denominator)
  2. 分子减去得到一个整数值,然后进行除法。mod
  3. 地板除法的结果是通过有效计算来计算的floor((numerator - mod) / denominator)
  4. 之后,@Kasramvd的回答中已经提到的检查已经完成。但这只会将结果捕捉到最接近的整数值。(numerator - mod) / denominator

这给出不同结果的原因是,由于浮点算术给出而不是 .因此,计算的结果实际上是,并且捕捉到最接近的整数值并不能修复 的“错误”结果引入的错误。你也可以很容易地在 c 中查出它:fmod(8.0, 0.4)0.40.0floor((8.0 - 0.4) / 0.4) = 19(8.0 - 0.4) / 0.4) = 19fmod

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    double mod = fmod(vx, wx);
    printf("%lf\n", mod);
    double div = (vx-mod)/wx;
    printf("%lf\n", div);
}

// gives:
// 0.4
// 19.000000

我猜,他们选择这种计算地板除法的方式来保持 的有效性(如@0x539回答中的链接中所述),尽管这现在导致了 .(numerator//divisor)*divisor + fmod(numerator, divisor) = numeratorfloor(8.0/0.4) != 8.0//0.4

评论

2赞 Kyle Strand 7/27/2016
你似乎是唯一有正确答案的人。道具!但是,由于您必须深入研究源代码才能找到它,我想知道这是否是所有 Python 实现的强制性部分?
2赞 Kyle Strand 7/27/2016
PEP 238开始,人们似乎确实期望这将是正确的,因为这被明确表述为“地板划分”的语义。floor(a/b) == a // b
1赞 jotasi 7/27/2016
在@0x539已经引用的问题报告(bugs.python.org/issue27463)中,它似乎没有被认为是错误的。这是 Python BugTracker。所以我想“楼层划分”更像是一个名称,而不是定义实现。
2赞 user2357112 7/27/2016
“地板除法的结果是通过有效计算来计算的”——不,它更像是.源代码确实使用了 ,但如果四舍五入的方式错误,它会立即向上调整结果。它依靠零件来“有效地铺设”。floor((numerator - mod) / denominator)round((numerator - mod) / denominator)floorfloor- modnumerator / denominator
1赞 jotasi 7/27/2016
@user2357112 你是对的。实际上,结果是 ed 而不仅仅是 ed。尽管如此,还是导致了奇怪的结果。roundfloor-mod