Python楼层划分中的舍入错误

0x5*_*539 37 python floating-point rounding python-2.7 python-3.x

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

这在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

编辑:这不是浮点数学的重复吗?因为我问为什么这个特定的操作受到(可能是可以避免的)舍入错误的影响,以及为什么a // b没有定义为/等于floor(a / b)

备注:我猜这个不起作用的深层原因是地板划分是不连续的,因此具有无限的条件数使其成为一个不适定的问题.地板划分和浮点数简单地基本上是不兼容的,你永远不应该使用//浮点数.只需使用整数或分数.

das*_*s-g 25

正如你和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
Run Code Online (Sandbox Code Playgroud)

然而

Fraction('8') // Fraction(0.4)
# 19
Run Code Online (Sandbox Code Playgroud)

在这里,0.4被解释为一个浮动文本(以及因此浮点二进制数)需要(二进制)的舍入,和只然后转化为有理数Fraction(3602879701896397, 9007199254740992),这几乎是但不完全4/10.然后执行地板划分,因为

19 * Fraction(3602879701896397, 9007199254740992) < 8.0
Run Code Online (Sandbox Code Playgroud)

20 * Fraction(3602879701896397, 9007199254740992) > 8.0
Run Code Online (Sandbox Code Playgroud)

结果是19,而不是20.

同样可能发生

8.0 // 0.4
Run Code Online (Sandbox Code Playgroud)

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

那么为什么呢

floor(8.0 / 0.4)
Run Code Online (Sandbox Code Playgroud)

给出"正确"的结果?因为那里,两个舍入误差相互抵消.首先1)执行除法,产生略小于20.0的值,但不能表示为浮点数.它会四舍五入到最近的浮点数,这恰好是20.0.只有然后,将floor执行的操作,但现在作用于准确 20.0,因此不换号了.


1)由于凯尔-斯特兰德指出,该确切的结果确定四舍五入不是什么实际发生的低2) -电平(CPython中的C代码,甚至CPU指令).然而,它可以是用于确定预期的一个有用的模型3)的结果.

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

3) Python规范的"预期",不一定是我们的直觉.

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

  • “似乎划分的分割是原子决定的” –很好的猜测,我想语义上是正确的,但是就实现必须做什么而言,这有点倒退:因为没有硬件支持“原子”`//`。在语义上,余数是预先计算的,并从分子中减去,以确保浮点除法(最终发生时)仅立即*计算*正确的值,而无需进一步调整。 (2认同)

shi*_*iva 11

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

但是,如果要阻止它,可以使用decimal基本上设计为表示十进制浮点数的模块,与二进制浮点表示形成对比.

所以在你的情况下,你可以这样做:

>>> from decimal import *
>>> Decimal('8.0')//Decimal('0.4')
Decimal('20')
Run Code Online (Sandbox Code Playgroud)

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


0x5*_*539 10

经过一些研究后我发现了这个问题.似乎正在发生的事情是,正如@khelwood建议在0.4内部进行评估0.40000000000000002220,当分割8.0得到的东西略小于20.0.该/操作者然后舍入为最接近的浮点数,其是20.0,但//操作者立即截断结果,得到19.0.

这应该更快,我认为它"接近处理器",但我仍然不是用户想要/期望的.

  • 很好找,那个.但是*用户想要什么呢?正确的数学行为对于本质上不正确的数字开始?(其中相同的平均"典型用户"[通常幸福地没有意识到](http://stackoverflow.com/questions/14368893/why-is-decimal-multiplication-slightly-inaccurate).) (7认同)
  • @ 0x539:依赖于`//`来截断略低于'20.0`到19.0的东西的穷人用户怎么样?这里的问题是用户想要进行精确的算术并且正在使用错误的工具来完成工作. (5认同)
  • 从数学上来说,您会正确地认为“ 8.0 / 0.40000000000000002220”会产生比“ 20.0”稍小的结果。但是,将浮点运算视为一系列步骤进行计算是不正确的,这些步骤中需要计算“实际实际数学值”,然后“然后*”四舍五入(当您说“ ./ *四舍五入...”)。当然,这是不可能的,因为计算机*必须*必须有一种内部表示计算的所有中间步骤的方法!请参阅@jotasi的答案。 (2认同)

Kas*_*mvd 9

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

>>> floor(8//0.4000000000000001)
19.0
Run Code Online (Sandbox Code Playgroud)

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

阅读Guido本人更多关于蟒蛇整数分区的信息.

另外,有关浮点数的完整信息,请阅读本文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);
}
Run Code Online (Sandbox Code Playgroud)

哪个最终结果将按功能计算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;
}
Run Code Online (Sandbox Code Playgroud)

  • "事实是它取决于可用的`PyFloatObjects`的大小" - 什么?不,不.`PyFloatObject的大小都相同,`PyFloatObject`s的存储细节几乎与任何这种行为无关. (2认同)

jot*_*asi 9

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

对于正常除法float_div被调用(第560行),它在内部将python floats 转换为c- doubles,进行除法,然后将得到的结果转换double为python float.如果您只是8.0/0.4在c中执行此操作,您将获得:

#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
Run Code Online (Sandbox Code Playgroud)

对于场地划分,还会发生其他事情.在内部,float_floor_div(第654行)被调用,然后调用float_divmod一个函数,该函数应该返回float包含内部除法的python s 元组,以及mod /余数,即使后者被抛弃PyTuple_GET_ITEM(t, 0).这些值按以下方式计算(转换为c- doubles后):

  1. 余数通过使用计算double mod = fmod(numerator, denominator).
  2. mod当你进行除法时,分子减去得到一个整数值.
  3. 通过有效计算来计算地板划分的结果 floor((numerator - mod) / denominator)
  4. 之后,@ Kasramvd的答案中已经提到的检查完成了.但这只会将结果捕捉(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到最接近的整数值并不能解决由"错误"结果引入的错误fmod.您也可以轻松地在c中查找:

#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
Run Code Online (Sandbox Code Playgroud)

我猜,他们选择这种计算浮动除法的方式来保持有效性(numerator//divisor)*divisor + fmod(numerator, divisor) = numerator(正如在@ 0x539的答案中的链接中所提到的),即使这现在导致了一些意想不到的行为floor(8.0/0.4) != 8.0//0.4.

  • 您似乎是唯一拥有正确答案的人。道具!不过,由于您必须深入研究源代码才能找到它,所以我想知道这是否是所有 Python 实现的强制部分? (2认同)
  • 看来,从[PEP 238](https://www.python.org/dev/peps/pep-0238/)开始,预计`floor(a/b)== a // b`会是的,因为这被明确地称为"地板划分"的语义. (2认同)
  • "通过有效计算`floor((numerator - mod)/ denominator)"来计算平面除法的结果."不,它更像是'round((numerator - mod)/ denominator)`.源代码确实使用`floor`,但如果`floor`以错误的方式舍入,它会立即向上调整结果.它依赖于` - mod`部分来"有效地""分子/分母". (2认同)