Một nhận xét trong mã nguồn Python cho các đối tượng float thừa nhận rằng:
So sánh là một cơn ác mộng
Điều này đặc biệt đúng khi so sánh số float với một số nguyên, bởi vì, không giống như số float, số nguyên trong Python có thể lớn tùy ý và luôn chính xác. Cố gắng truyền số nguyên cho một số float có thể làm mất độ chính xác và làm cho phép so sánh không chính xác. Cố gắng chuyển float sang một số nguyên cũng sẽ không hoạt động vì bất kỳ phần phân số nào cũng sẽ bị mất.
Để giải quyết vấn đề này, Python thực hiện một loạt các kiểm tra, trả về kết quả nếu một trong các kiểm tra thành công. Nó so sánh các dấu hiệu của hai giá trị, sau đó liệu số nguyên có "quá lớn" là một số float hay không, sau đó so sánh số mũ của số float với chiều dài của số nguyên. Nếu tất cả các kiểm tra này không thành công, cần phải xây dựng hai đối tượng Python mới để so sánh để có được kết quả.
Khi so sánh số float v
với số nguyên / dài w
, trường hợp xấu nhất là:
v
và w
có cùng dấu (cả dương hoặc cả âm),
- số nguyên
w
có ít bit đủ để có thể được giữ trong size_t
loại (thường là 32 hoặc 64 bit),
- số nguyên
w
có ít nhất 49 bit,
- số mũ của float
v
giống như số bit trong w
.
Và đây chính xác là những gì chúng ta có cho các giá trị trong câu hỏi:
>>> import math
>>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair
(0.9999999999976706, 49)
>>> (562949953421000).bit_length()
49
Chúng ta thấy 49 là cả số mũ của số float và số bit trong số nguyên. Cả hai số đều dương và vì vậy bốn tiêu chí trên được đáp ứng.
Việc chọn một trong các giá trị lớn hơn (hoặc nhỏ hơn) có thể thay đổi số bit của số nguyên hoặc giá trị của số mũ và do đó Python có thể xác định kết quả so sánh mà không cần thực hiện kiểm tra cuối cùng đắt tiền.
Điều này là cụ thể cho việc thực hiện CPython của ngôn ngữ.
Sự so sánh chi tiết hơn
Các float_richcompare
chức năng xử lý sự so sánh giữa hai giá trị v
và w
.
Dưới đây là mô tả từng bước của các kiểm tra mà chức năng thực hiện. Các ý kiến trong nguồn Python thực sự rất hữu ích khi cố gắng hiểu chức năng này làm gì, vì vậy tôi đã để chúng ở nơi có liên quan. Tôi cũng đã tóm tắt những kiểm tra này trong một danh sách dưới chân câu trả lời.
Ý tưởng chính là ánh xạ các đối tượng Python v
và w
hai nhân đôi C thích hợp, i
và j
sau đó có thể dễ dàng so sánh để đưa ra kết quả chính xác. Cả Python 2 và Python 3 đều sử dụng cùng một ý tưởng để thực hiện điều này (trước đây chỉ xử lý int
và long
gõ riêng).
Điều đầu tiên cần làm là kiểm tra xem đó có phải v
là một float Python và ánh xạ nó tới C double i
. Tiếp theo, hàm xem xét xem có phải w
là số float hay không và ánh xạ nó tới C double j
. Đây là trường hợp tốt nhất cho chức năng vì tất cả các kiểm tra khác có thể được bỏ qua. Kiểm tra chức năng cũng để xem liệu v
là inf
hay 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;
}
Bây giờ chúng tôi biết rằng nếu w
thất bại các kiểm tra này, nó không phải là một float Python. Bây giờ hàm kiểm tra xem đó có phải là số nguyên Python không. Nếu đây là trường hợp, thử nghiệm đơn giản nhất là trích xuất dấu hiệu v
và dấu hiệu của w
(trả về 0
nếu bằng 0, -1
nếu âm, 1
nếu dương). Nếu các dấu hiệu khác nhau, đây là tất cả thông tin cần thiết để trả về kết quả so sánh:
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;
}
}
Nếu kiểm tra này thất bại, sau đó v
và w
có cùng một dấu hiệu.
Kiểm tra tiếp theo đếm số bit trong số nguyên w
. Nếu nó có quá nhiều bit thì nó không thể được giữ như một float và do đó phải có độ lớn lớn hơn float 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;
}
Mặt khác, nếu số nguyên w
có 48 bit trở xuống, nó có thể biến thành một C một cách an toàn j
và so sánh:
if (nbits <= 48) {
j = PyLong_AsDouble(w);
/* It's impossible that <= 48 bits overflowed. */
assert(j != -1.0 || ! PyErr_Occurred());
goto Compare;
}
Từ thời điểm này trở đi, chúng ta biết rằng w
có 49 bit trở lên. Sẽ thuận tiện khi coi w
là số nguyên dương, vì vậy hãy thay đổi dấu và toán tử so sánh khi cần thiết:
if (nbits <= 48) {
/* "Multiply both sides" by -1; this also swaps the
* comparator.
*/
i = -i;
op = _Py_SwappedOp[op];
}
Bây giờ chức năng nhìn vào số mũ của phao. Hãy nhớ lại rằng một dấu phẩy có thể được viết (bỏ qua dấu hiệu) là có ý nghĩa * 2 số mũ và ý nghĩa đó đại diện cho một số trong khoảng từ 0,5 đến 1:
(void) frexp(i, &exponent);
if (exponent < 0 || (size_t)exponent < nbits) {
i = 1.0;
j = 2.0;
goto Compare;
}
Điều này kiểm tra hai điều. Nếu số mũ nhỏ hơn 0 thì số float nhỏ hơn 1 (và vì vậy độ lớn nhỏ hơn bất kỳ số nguyên nào). Hoặc, nếu số mũ nhỏ hơn số bit trong w
đó thì chúng ta có số đó v < |w|
kể từ khi có ý nghĩa * 2 số mũ vì số mũ nhỏ hơn 2 nbits .
Không thực hiện hai kiểm tra này, hàm sẽ xem liệu số mũ có lớn hơn số bit trong hay không w
. Điều này cho thấy số mũ có ý nghĩa * 2 lớn hơn 2 nbits và do đóv > |w|
:
if ((size_t)exponent > nbits) {
i = 2.0;
j = 1.0;
goto Compare;
}
Nếu kiểm tra này không thành công, chúng tôi biết rằng số mũ của số float v
giống như số bit trong số nguyên w
.
Cách duy nhất mà hai giá trị có thể được so sánh bây giờ là xây dựng hai số nguyên Python mới từ v
và w
. Ý tưởng là loại bỏ phần phân số của v
, nhân đôi phần nguyên và sau đó thêm một phần. w
cũng được nhân đôi và hai đối tượng Python mới này có thể được so sánh để đưa ra giá trị trả về chính xác. Sử dụng một ví dụ với các giá trị nhỏ, 4.65 < 4
sẽ được xác định bằng cách so sánh (2*4)+1 == 9 < 8 == (2*4)
(trả về false).
{
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
}
}
Để cho ngắn gọn, tôi đã bỏ qua Python kiểm tra lỗi và theo dõi rác bổ sung phải làm khi nó tạo ra các đối tượng mới này. Không cần phải nói, điều này bổ sung thêm chi phí và giải thích tại sao các giá trị được tô sáng trong câu hỏi chậm hơn đáng kể so với các giá trị khác.
Dưới đây là một bản tóm tắt các kiểm tra được thực hiện bởi chức năng so sánh.
Hãy v
là một cái phao và đúc nó thành một đôi C. Bây giờ nếuw
cũng là một float:
Kiểm tra xem w
là nan
hay inf
. Nếu vậy, xử lý riêng trường hợp đặc biệt này tùy thuộc vào loại w
.
Nếu không, so sánh v
và w
trực tiếp bằng cách biểu diễn của chúng khi C nhân đôi.
Nếu w
là một số nguyên:
Trích xuất các dấu hiệu của v
và w
. Nếu chúng khác nhau thì chúng ta biết v
và w
khác biệt và đó là giá trị lớn hơn.
( Các dấu hiệu giống nhau. ) Kiểm tra xem w
có quá nhiều bit để làm nổi hay không (nhiều hơn size_t
). Nếu vậy, w
có cường độ lớn hơn v
.
Kiểm tra nếu w
có 48 bit hoặc ít hơn. Nếu vậy, nó có thể được đúc một cách an toàn đến gấp đôi C mà không mất độ chính xác và so vớiv
.
( w
có hơn 48 bit. Bây giờ chúng ta sẽ coi w
là một số nguyên dương đã thay đổi op so sánh cho phù hợp. )
Hãy xem xét số mũ của phao v
. Nếu số mũ là âm, thì v
nhỏ hơn 1
và do đó nhỏ hơn bất kỳ số nguyên dương nào. Khác, nếu số mũ nhỏ hơn số bit trong w
thì nó phải nhỏ hơn w
.
Nếu số mũ của v
lớn hơn số bit trong w
thì v
lớn hơn w
.
( Số mũ giống như số bit trong w
. )
Kiểm tra cuối cùng. Chia v
thành các phần nguyên và phần của nó. Nhân đôi phần nguyên và thêm 1 để bù cho phần phân số. Bây giờ nhân đôi số nguyên w
. Thay vào đó, so sánh hai số nguyên mới này để có kết quả.