Python中的float对象源码中有一条注释承认:
比较几乎是一场噩梦
尤其是在将float与整数进行比较时,这一点尤为明显。因为与浮点数不同,Python中的整数可以任意大且始终精确。将整数强制转换为浮点数可能会导致精度丢失并使比较不准确。试图将浮点数转换为整数也行不通,因为任何小数部分都将被丢失。
为了解决这个问题,Python执行一系列检查,如果其中一个检查成功,则返回结果。它比较两个值的符号,然后比较整数是否“太大”无法转换为浮点数,然后将浮点数的指数与整数的长度进行比较。如果所有这些检查都失败,则需要构造两个新的Python对象进行比较以获得结果。
当将浮点数v
与整数/长整数w
进行比较时,最坏的情况是:
v
和w
具有相同的符号(都是正数或负数),
- 整数
w
的位数足够少,可以保存在size_t
类型中(通常为32位或64位),
- 整数
w
至少有49位,
- 浮点数
v
的指数与w
的位数相同。
这恰好是问题中的值所具有的特征:
>>> import math
>>> math.frexp(562949953420000.7)
(0.9999999999976706, 49)
>>> (562949953421000).bit_length()
49
我们可以看到,49既是浮点数的指数,也是整数位数。这两个数字都是正数,因此满足上述四个标准。
选择其中一个值更大(或更小)可能会改变整数的位数或指数的值,因此Python能够在不执行昂贵的最终检查的情况下确定比较结果。
这仅适用于语言的CPython实现。
更详细的比较
float_richcompare
函数处理两个值v
和w
之间的比较。
下面是函数执行的逐步说明。Python源代码中的注释在理解函数操作时非常有用,因此我已将相关注释保留在其中。我还在答案底部总结了这些检查的列表。
主要思想是将Python对象v
和w
映射到两个适当的C双精度浮点数i
和j
,然后可以轻松地进行比较以得出正确的结果。 Python 2和Python 3都使用相同的思想来实现此目的(前者只是单独处理int
和long
类型)。
首先要做的是检查
v
是否确实是Python浮点数,并将其映射到C双精度
i
。接下来,函数会检查
w
是否也是浮点数,并将其映射到C双精度
j
。这是函数的最佳情况,因为所有其他检查都可以跳过。函数还会检查
v
是否为
inf
或
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) {
i = (double)vsign;
j = (double)wsign;
goto Compare;
}
}
如果这个检查失败了,那么
v
和
w
的符号是相同的。
接下来的检查计算整数
w
中的位数。如果它有太多位,则不可能作为浮点数保存,因此必须比浮点数
v
具有更大的数量级。
nbits = _PyLong_NumBits(w);
if (nbits == (size_t)-1 && PyErr_Occurred()) {
PyErr_Clear();
i = (double)vsign;
assert(wsign != 0);
j = wsign * 2.0;
goto Compare;
}
另一方面,如果整数“w”有48位或更少,它可以安全地转换为C双精度“j”进行比较:
if (nbits <= 48) {
j = PyLong_AsDouble(w);
assert(j != -1.0 || ! PyErr_Occurred());
goto Compare;
}
从这一点开始,我们知道w
具有49位或更多位。为了方便起见,将w
视为正整数,必要时更改符号和比较运算符:
if (nbits <= 48) {
i = -i;
op = _Py_SwappedOp[op];
}
现在该函数查看浮点数的指数。回想一下,一个浮点数可以写成(忽略符号)significand * 2
exponent,significand代表0.5到1之间的数字:
(void) frexp(i, &exponent);
if (exponent < 0 || (size_t)exponent < nbits) {
i = 1.0;
j = 2.0;
goto Compare;
}
这里检查了两件事情。如果指数小于0,则浮点数小于1(因此比任何整数的幅值都要小)。或者,如果指数小于
w
的位数,则有
v < |w|
,因为significand * 2
exponent小于2
nbits。
如果这两个检查失败,函数会查看指数是否大于
w
的位数。这表明significand * 2
exponent大于2
nbits,因此
v > |w|
。
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
如果这个检查没有成功,我们知道浮点数
v
的指数与整数
w
中的位数相同。现在唯一比较这两个值的方法是从
v
和
w
构建两个新的 Python 整数。思路是丢弃
v
的小数部分,将整数部分加倍,然后再加一。
w
也会被加倍,这两个新的 Python 对象可以进行比较以得到正确的返回值。使用小值示例,
4.65 < 4
将通过比较
(2*4)+1 == 9 < 8 == (2*4)
(返回 false)来确定。
{
double fracpart;
double intpart;
PyObject *result = NULL;
PyObject *one = NULL;
PyObject *vv = NULL;
PyObject *ww = w;
fracpart = modf(i, &intpart);
vv = PyLong_FromDouble(intpart);
if (fracpart != 0.0) {
PyObject *temp;
one = PyLong_FromLong(1);
temp = PyNumber_Lshift(ww, one);
ww = temp;
temp = PyNumber_Lshift(vv, one);
vv = temp;
temp = PyNumber_Or(vv, one);
vv = temp;
}
}
}
为了简洁起见,我在创建这些新对象时省略了Python必须进行的额外错误检查和垃圾跟踪。不用说,这增加了额外的开销,并解释了为什么问题中突出显示的值比其他值慢得多。
以下是比较函数执行的检查摘要。
设
v
为浮点数并将其转换为 C 双精度。现在,如果
w
也是浮点数:
- 检查
w
是否为 nan
或 inf
。如果是,则根据 w
的类型分别处理此特殊情况。
- 如果不是,则直接通过它们作为 C 双精度的表示来比较
v
和 w
。
如果
w
是整数:
提取 v
和 w
的符号。如果它们不同,则我们知道 v
和 w
不同,并且哪个值更大。
(符号相同。) 检查 w
是否有太多位数无法转换为浮点数(超过 size_t
)。如果是,则 w
的幅度比 v
大。
检查 w
是否有 48 位或更少。如果是,则可以安全地将其转换为 C double,而不会失去精度,并与 v
进行比较。
(w
多于 48 位。现在,我们将把 w
视为一个正整数,并根据需要更改比较运算符。)
考虑浮点数 v
的指数。如果指数为负,则 v
小于 1
,因此小于任何正整数。否则,如果指数小于 w
的位数,则必须小于 w
。
如果 v
的指数大于 w
的位数,则 v
大于 w
。
(指数与 w
的位数相同。)
最后的检查。将 v
分成整数部分和小数部分。将整数部分加倍并加 1 以补偿小数部分。现在将整数 w
加倍。比较这两个新整数以获取结果。