Python 源代码中关于 float 对象的注释承认:
比较几乎是一场噩梦 https://hg.python.org/cpython/file/ea33b61cac74/Objects/floatobject.c#l285
在将浮点数与整数进行比较时尤其如此,因为与浮点数不同,Python 中的整数可以任意大并且始终是精确的。尝试将整数转换为浮点数可能会丢失精度并使比较不准确。尝试将浮点数转换为整数也不起作用,因为任何小数部分都会丢失。
为了解决这个问题,Python 执行一系列检查,如果其中一个检查成功则返回结果。它比较两个值的符号,然后比较整数是否“太大”而不能成为浮点数,然后比较浮点数的指数与整数的长度。如果所有这些检查都失败,则需要构造两个新的Python对象进行比较才能获得结果。
比较浮点数时v
为整数/长整型w
,最坏的情况是:
-
v
and w
具有相同的符号(均为正或均为负),
- 整数
w
有足够少的位可以保存在size_t https://stackoverflow.com/a/2550799/3923281类型(通常为 32 或 64 位),
- 整数
w
至少有 49 位,
- 浮点数的指数
v
与中的位数相同w
.
这正是我们对问题中的值的理解:
>>> import math
>>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair
(0.9999999999976706, 49)
>>> (562949953421000).bit_length()
49
我们看到 49 既是浮点数的指数,也是整数的位数。两个数字都是正数,因此满足上述四个标准。
选择较大(或较小)的值之一可以更改整数的位数或指数的值,因此 Python 能够确定比较的结果,而无需执行昂贵的最终检查。
这是特定于该语言的 CPython 实现的。
更详细的比较
The float_richcompare https://hg.python.org/cpython/file/ea33b61cac74/Objects/floatobject.c#l301函数处理两个值之间的比较v
and w
.
以下是该函数执行的检查的分步说明。当尝试理解函数的作用时,Python 源代码中的注释实际上非常有帮助,因此我将它们保留在相关的位置。我还在答案底部的列表中总结了这些检查。
主要思想是映射Python对象v
and w
到两个适当的 C 双打,i
and j
,然后可以轻松比较以给出正确的结果。 Python 2 和 Python 3 都使用相同的想法来做到这一点(前者只处理int
and long
分别类型)。
首先要做的是检查v
绝对是一个 Python float 并将其映射到一个 C doublei
。接下来该函数查看是否w
也是一个 float 并将其映射到 C doublej
。这是该函数的最佳情况,因为可以跳过所有其他检查。该函数还检查是否v
is inf
or nan
:
static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{
double i, j;
int r = 0;
assert(PyFloat_Check(v));
i = PyFloat_AS_DOUBLE(v);
if (PyFloat_Check(w))
j = PyFloat_AS_DOUBLE(w);
else if (!Py_IS_FINITE(i)) {
if (PyLong_Check(w))
j = 0.0;
else
goto Unimplemented;
}
现在我们知道如果w
未通过这些检查,它不是 Python 浮点数。现在该函数检查它是否是一个 Python 整数。如果是这种情况,最简单的测试是提取v
和标志w
(返回0
如果为零,-1
如果为负,1
如果是积极的)。如果符号不同,则返回比较结果所需的全部信息如下:
else if (PyLong_Check(w)) {
int vsign = i == 0.0 ? 0 : i < 0.0 ? -1 : 1;
int wsign = _PyLong_Sign(w);
size_t nbits;
int exponent;
if (vsign != wsign) {
/* Magnitudes are irrelevant -- the signs alone
* determine the outcome.
*/
i = (double)vsign;
j = (double)wsign;
goto Compare;
}
}
如果此检查失败,则v
and w
有相同的标志。
下一个检查计算整数中的位数w
。如果它有太多位,那么它不可能作为浮点数保存,因此其大小必须大于浮点数v
:
nbits = _PyLong_NumBits(w);
if (nbits == (size_t)-1 && PyErr_Occurred()) {
/* This long is so large that size_t isn't big enough
* to hold the # of bits. Replace with little doubles
* that give the same outcome -- w is so large that
* its magnitude must exceed the magnitude of any
* finite float.
*/
PyErr_Clear();
i = (double)vsign;
assert(wsign != 0);
j = wsign * 2.0;
goto Compare;
}
另一方面,如果整数w
有 48 位或更少,它可以安全地转入 C doublej
并比较:
if (nbits <= 48) {
j = PyLong_AsDouble(w);
/* It's impossible that <= 48 bits overflowed. */
assert(j != -1.0 || ! PyErr_Occurred());
goto Compare;
}
从此时开始,我们知道w
有 49 位或更多位。治疗起来会很方便w
作为正整数,因此根据需要更改符号和比较运算符:
if (nbits <= 48) {
/* "Multiply both sides" by -1; this also swaps the
* comparator.
*/
i = -i;
op = _Py_SwappedOp[op];
}
Now the function looks at the exponent of the float. Recall that a float can be written (ignoring sign) as significand * 2exponent and that the significand represents a number between 0.5 and 1:
(void) frexp(i, &exponent);
if (exponent < 0 || (size_t)exponent < nbits) {
i = 1.0;
j = 2.0;
goto Compare;
}
This checks two things. If the exponent is less than 0 then the float is smaller than 1 (and so smaller in magnitude than any integer). Or, if the exponent is less than the number of bits in w
then we have that v < |w|
since significand * 2exponent is less than 2nbits.
Failing these two checks, the function looks to see whether the exponent is greater than the number of bit in w
. This shows that significand * 2exponent is greater than 2nbits and so v > |w|
:
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
如果此检查没有成功,我们知道浮点数的指数v
与整数的位数相同w
.
现在比较这两个值的唯一方法是构造两个新的 Python 整数v
and w
。这个想法是丢弃小数部分v
,将整数部分加倍,然后加一。w
也加倍,可以比较这两个新的 Python 对象以给出正确的返回值。使用较小值的示例,4.65 < 4
将通过比较来确定(2*4)+1 == 9 < 8 == (2*4)
(返回错误)。
{
double fracpart;
double intpart;
PyObject *result = NULL;
PyObject *one = NULL;
PyObject *vv = NULL;
PyObject *ww = w;
// snip
fracpart = modf(i, &intpart); // split i (the double that v mapped to)
vv = PyLong_FromDouble(intpart);
// snip
if (fracpart != 0.0) {
/* Shift left, and or a 1 bit into vv
* to represent the lost fraction.
*/
PyObject *temp;
one = PyLong_FromLong(1);
temp = PyNumber_Lshift(ww, one); // left-shift doubles an integer
ww = temp;
temp = PyNumber_Lshift(vv, one);
vv = temp;
temp = PyNumber_Or(vv, one); // a doubled integer is even, so this adds 1
vv = temp;
}
// snip
}
}
为了简洁起见,我省略了 Python 在创建这些新对象时必须执行的额外错误检查和垃圾跟踪。不用说,这会增加额外的开销,并解释了为什么问题中突出显示的值的比较速度明显慢于其他值。
以下是比较函数执行的检查的摘要。
Let v
是一个 float 并将其转换为 C double。现在,如果w
也是一个浮点数:
If w
是一个整数:
提取符号v
and w
。如果它们不同那么我们就知道v
and w
不同,哪个价值更大。
(迹象是一样的。) 检查是否w
有太多位无法成为浮点数(超过size_t
)。如果是这样,w
幅度大于v
.
检查是否w
具有 48 位或更少。如果是这样,它可以安全地转换为 C double,而不会损失其精度,并与v
.
(w
超过 48 位。我们现在将治疗w
作为一个正整数,适当地改变了比较操作。)
考虑浮点数的指数v
。如果指数为负数,则v
小于1
因此小于任何正整数。否则,如果指数小于中的位数w
那么它一定小于w
.
如果指数为v
大于位数w
then v
大于w
.
(指数与位数相同w
.)
最后检查。分裂v
分为整数部分和小数部分。将整数部分加倍并加 1 以补偿小数部分。现在将整数加倍w
。比较这两个新整数即可得到结果。