Python楼层划分中的舍入错误

时间:2016-07-26 11:35:11

标签: python python-2.7 python-3.x floating-point rounding

我知道在浮点运算中会发生舍入错误,但有人可以解释这个问题的原因:

>>> 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 // b不能简单地定义为floor(a / b)

编辑8.0 % 0.4也评估为0.3999999999999996。至少这是因为8.0 // 0.4 * 0.4 + 8.0 % 0.4评估为8.0

的结果

编辑:这不是Is floating point math broken?的重复,因为我问为什么这个特定的操作会受到(可能是可以避免的)舍入错误的影响,以及为什么a // b isn' t定义为/等于floor(a / b)

备注:我认为这不起作用的深层原因是地板划分是不连续的,因此有一个无限condition number使它成为一个不适定的问题。楼层划分和浮点数根本上是不兼容的,你永远不应该在浮点数上使用//。只需使用整数或分数。

5 个答案:

答案 0 :(得分:24)

正如你和khelwood已经注意到的那样,0.4不能完全表示为浮点数。为什么?它是五分之二(4/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

这里,0.4被解释为浮点文字(因而是浮点二进制数),它需要(二进制)舍入,只有然后转换为有理数{{1这几乎是但不完全是4/10。然后执行了分区,因为

Fraction(3602879701896397, 9007199254740992)

19 * Fraction(3602879701896397, 9007199254740992) < 8.0

结果是19,而不是20。

可能会发生同样的情况
20 * Fraction(3602879701896397, 9007199254740992) > 8.0

即,似乎浮动除法是以原子方式确定的(但是在解释的浮点文字的唯一近似浮点值上)。

那为什么呢

8.0 // 0.4

给出“正确”的结果?因为那里,两个舍入误差相互抵消。 首先 1)执行除法,产生略小于20.0的值,但不能表示为float。它会四舍五入到最近的浮点数,恰好是floor(8.0 / 0.4) 。只有然后,才会执行20.0操作,但现在正在完全 floor,因此不再更改数字。

1)作为Kyle Strand points out,确切的结果然后舍入 实际上发生低 2) -level(CPython的C代码甚至是CPU指令)。但是,它可以是确定预期 3)结果的有用模型。

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

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

4)那么,最低级高于逻辑门。我们不必考虑使半导体有可能理解这一点的量子力学。

答案 1 :(得分:11)

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

但是如果你想阻止它,你可以使用decimal模块,它基本上是为了表示与二进制浮点表示形成鲜明对比的十进制浮点数。

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

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

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

答案 2 :(得分:10)

经过一些研究后我发现了issue。 似乎正在发生的事情是,@ khelwood建议0.4在内部对0.40000000000000002220进行评估,在8.0除以后会产生略小于20.0的内容。 /运算符然后舍入到最近的浮点数20.0,但//运算符会立即截断结果,产生19.0

这应该更快,我认为它接近处理器&#34;但我仍然不是用户想要/正在期待的东西。

答案 3 :(得分:9)

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

>>> floor(8//0.4000000000000001)
19.0

但真正的除法(/returns a reasonable approximation of the division result if the arguments are floats or complex.这就是8.0/0.4的结果为20的原因。它实际上取决于参数的大小(在C双参数中)。 (不会舍入到最近的浮点数

阅读Guido本人关于pythons integer division floors的更多信息。

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

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

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;
}

答案 4 :(得分:7)

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

对于正常分区float_div被调用(第560行),它在内部将python float转换为c - double s,进行除法,然后转换结果{{1} }回到python double。如果您只是在c中使用float执行此操作,则会获得:

8.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行)被调用,然后调用float_floor_div,这个函数应该返回包含被覆地区的python float_divmod的元组,以及mod /余数,即使后者被float抛弃了。这些值按以下方式计算(转换为c - PyTuple_GET_ITEM(t, 0)后):

  1. 剩余部分使用double
  2. 计算
  3. 当你进行除法时,分子减少double mod = fmod(numerator, denominator)以得到一个整数值。
  4. 通过有效计算mod
  5. 计算分层分割的结果
  6. 之后,@ Kasramvd的答案中已经提到的检查完成了。但这只会将floor((numerator - mod) / denominator)的结果捕捉到最接近的整数值。
  7. 这给出了不同结果的原因是,(numerator - mod) / denominator由于浮点运算而导致fmod(8.0, 0.4)而不是0.4。因此,计算的结果实际上是0.0并且将floor((8.0 - 0.4) / 0.4) = 19捕捉到最接近的整数值并不能解决由(8.0 - 0.4) / 0.4) = 19的“错误”结果引入的错误。您也可以轻松地在c中查找:

    fmod

    我猜想,他们选择这种计算地板分区的方式来保持#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) = numerator