提问人:0x539 提问时间:7/26/2016 最后编辑:wjandrea0x539 更新时间:7/27/2023 访问量:10737
Python floor division 中的舍入错误
rounding errors in Python floor division
问:
我知道在浮点运算中会发生舍入错误,但有人可以解释一下原因吗:
>>> 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)
EDIT: 的计算结果也为 .至少这是结果,因为那时评估为8.0 % 0.4
0.3999999999999996
8.0 // 0.4 * 0.4 + 8.0 % 0.4
8.0
编辑:这不是浮点数学是否损坏的重复? 因为我在问为什么这个特定的操作会受到(也许是可以避免的)舍入误差的影响,以及为什么不定义为 / 等于a // b
floor(a / b)
备注:我猜这不起作用的更深层次原因是楼层划分是不连续的,因此具有无限的条件数,使其成为一个不合理的问题。楼层除法和浮点数从根本上是不兼容的,您永远不应该在浮点数上使用。只需改用整数或分数即可。//
答:
那是因为 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;
}
评论
3./4.
1
float_div
float_divmod
float_floor_div
19
20
float_div
8.0/0.4 = 20
double
floor((8.0 - fmod(8.0, 0.4)) / 0.4) = 19
fmod(8.0, 0.4) = 0.4
PyFloatObjects
PyFloatObject
PyFloatObject
好的,经过一番研究,我发现了这个问题。
似乎正在发生的事情是,正如@khelwood建议的那样,在内部计算为 ,当除法时,它会产生比 略小的东西。然后,运算符四舍五入到最接近的浮点数,即 ,但运算符立即截断结果,生成 。0.4
0.40000000000000002220
8.0
20.0
/
20.0
//
19.0
这应该更快,我想它“接近处理器”,但我仍然不是用户想要/期望的。
评论
20.0
//
20.0
19.0
floor((8.0 - fmod(8.0, 0.4)) / 0.4)
fmod(8.0, 0.4)=0.4
8.0 / 0.40000000000000002220
20.0
/
@jotasi解释了其背后的真正原因。
但是,如果你想防止它,你可以使用模块,它基本上被设计为完全表示十进制浮点数,而不是二进制浮点表示。decimal
因此,在您的情况下,您可以执行以下操作:
>>> from decimal import *
>>> Decimal('8.0')//Decimal('0.4')
Decimal('20')
参考: https://docs.python.org/2/library/decimal.html
评论
decimal
fractions
模块似乎也在做这项工作。
正如您和 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
在这里,被解释为一个浮点文字(因此是一个浮点二进制数),它需要(二进制)舍入,然后才转换为有理数,它几乎但不完全是 4 / 10。然后执行地板除法,并且因为0.4
Fraction(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.0
floor
20.0
1)正如凯尔·斯特兰德(Kyle Strand)所指出的,确定然后四舍五入的确切结果并不是实际发生的低2)级(CPython的C代码甚至CPU指令)。但是,它可以是确定预期 3) 结果的有用模型。
2)然而,在最低的4)级别上,这可能不会太远。一些芯片组通过首先计算更精确(但仍然不准确,只是具有更多二进制数字)内部浮点结果,然后四舍五入到IEEE双精度来确定浮点结果。
3)Python规范“预期”,不一定是我们的直觉。
4)嗯,逻辑门上方的最低级别。我们不必考虑使半导体成为可能的量子力学来理解这一点。
评论
//
//
8.0//0.4
round((8.0 - fmod(8.0, 0.4)) / 0.4)
19
fmod(8.0/0.4)
0.4
在 github (https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c) 上检查了 cpython 中浮点对象的半官方源代码后,可以理解这里发生了什么。
对于普通除法称为(第 560 行),它在内部将 python s 转换为 c-s,进行除法,然后将结果转换回 python。如果你只是在 c 中这样做,你会得到:float_div
float
double
double
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 行)被调用,然后调用 ,该函数应该返回一个包含地板除法以及 mod/余数的 python s 元组,即使后者只是被 抛弃了。这些值的计算方式如下(转换为 c-s 后):float_floor_div
float_divmod
float
PyTuple_GET_ITEM(t, 0)
double
- 余数是使用 计算的。
double mod = fmod(numerator, denominator)
- 分子减去得到一个整数值,然后进行除法。
mod
- 地板除法的结果是通过有效计算来计算的
floor((numerator - mod) / denominator)
- 之后,@Kasramvd的回答中已经提到的检查已经完成。但这只会将结果捕捉到最接近的整数值。
(numerator - mod) / denominator
这给出不同结果的原因是,由于浮点算术给出而不是 .因此,计算的结果实际上是,并且捕捉到最接近的整数值并不能修复 的“错误”结果引入的错误。你也可以很容易地在 c 中查出它: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
#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
floor(8.0/0.4) != 8.0//0.4
评论
floor((numerator - mod) / denominator)
round((numerator - mod) / denominator)
floor
floor
- mod
numerator / denominator
round
floor
-mod
评论
'%.20f'%0.4
'0.40000000000000002220'
0.4
0.4
floor(8.0/0.4)
float
//
%
float
//
Decimal
floor(8.0/0.4)
8.0//0.4