Коментар до вихідного коду Python для плаваючих об'єктів визнає, що:
Порівняння - це майже кошмар
Особливо це стосується порівняння поплавця з цілим числом, оскільки, на відміну від плавців, цілі числа в Python можуть бути довільно великими і завжди точними. Спроба передати ціле число на поплавок може втратити точність і зробити порівняння неточним. Спроба передати поплавок на ціле число не вийде, тому що будь-яка дробова частина буде втрачена.
Щоб подолати цю проблему, Python виконує серію перевірок, повертаючи результат, якщо один із перевірок успішний. Він порівнює знаки двох значень, потім, чи є ціле число "занадто великим", щоб бути поплавком, а потім порівнює показник поплавця з довжиною цілого числа. Якщо всі ці перевірки виявляються невдалими, для порівняння необхідно побудувати два нові об’єкти Python.
Порівнюючи поплавок v
на ціле / довге w
, найгірший випадок:
v
і w
мають однаковий знак (як позитивний, так і негативний),
- ціле число
w
має декілька достатньо бітів, які можуть бути збережені у size_t
типі (як правило, 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 реалізації мови.
Порівняння більш детально
float_richcompare
Функція обробляє порівняння між двома значеннями v
і w
.
Нижче покроковий опис перевірок, які виконує функція. Коментарі в джерелі Python насправді дуже корисні при спробі зрозуміти, що робить функція, тому я залишив їх там, де це доречно. Я також узагальнив ці перевірки у списку біля підніжжя відповіді.
Основна ідея полягає у відображенні об'єктів Python v
та w
двох відповідних парних C, i
і j
, які потім можна легко порівняти, щоб дати правильний результат. І Python 2, і Python 3 використовують для цього однакові ідеї (колишні просто обробляють int
і long
вводять окремо).
Перше, що потрібно зробити, це перевірити, що v
, безумовно, є поплавком Python, і відобразити його на C double 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) {
/* Magnitudes are irrelevant -- the signs alone
* determine the outcome.
*/
i = (double)vsign;
j = (double)wsign;
goto Compare;
}
}
Якщо ця перевірка виявилася невдалою, тоді v
і 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 подвійне j
і порівнювати:
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];
}
Тепер функція дивиться на експонент поплавця. Нагадаємо, що поплавок може бути записаний (ігноруючи знак) як ознака * показник * 2, і що означення означає число від 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|
так як мантиса * 2 показник становить менше 2 Nbits .
Якщо не виконати ці дві перевірки, функція перевіряє, чи більше показника, ніж кількість бітів w
. Це показує , що мантиса * 2 експонента більше 2 Nbits і так v > |w|
:
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
Якщо ця перевірка не вдалася, ми знаємо, що показник поплавця v
такий самий, як і кількість бітів у цілому w
.
Єдиний спосіб порівняння двох значень зараз - це побудова двох нових цілих чисел Python з v
і 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, коли він створює ці нові об’єкти. Зайве говорити, що це додає додаткових накладних витрат і пояснює, чому значення, виділені у питанні, значно повільніше порівняти, ніж інші.
Ось підсумок перевірок, які виконуються функцією порівняння.
Дозвольте v
бути поплавком і відливати його як C подвійний. Тепер, якщо w
також є поплавок:
Перевірте , чи правильно чи w
це nan
або inf
. Якщо так, обробляйте цей спеціальний чохол окремо, залежно від типу w
.
Якщо ні, порівняйте v
та w
безпосередньо їх представлення як C подвійне.
Якщо w
це ціле число:
Витягнути ознаки v
і w
. Якщо вони різні, то ми знаємо v
і w
відрізняємося, і чим більша цінність.
( Знаки однакові. ) Перевірте, чи w
є занадто багато бітів, щоб бути плавцем (більше, ніж size_t
). Якщо це так, w
має велику величину , ніж v
.
Перевірте, чи w
має 48 або менше біт. Якщо це так, його можна сміливо передати на подвійний C, не втрачаючи точності і порівняти з v
.
( w
має більше 48 біт. Тепер ми будемо розглядатись w
як додатне ціле число, змінивши опцію порівняння відповідно. )
Розглянемо показник поплавця v
. Якщо показник від'ємний, то v
менший 1
і, отже, менший за будь-яке додатне ціле число. В іншому випадку, якщо показник менший за кількість бітів, w
то він повинен бути меншим за w
.
Якщо показник показника v
більше, ніж кількість бітів, w
то v
більший за w
.
( Експонент такий самий, як кількість бітів в w
. )
Заключна перевірка. Розділіть v
на цілі та дробові частини. Подвійно цілу частину і додай 1 для компенсації дробової частини. Тепер подвоїмо ціле число w
. Порівняйте замість цих двох нових цілих чисел, щоб отримати результат.