Pull to refresh

Comments 33

Любопытно, конечно, но программисту на Питоне это знать нет необходимости.

А нужно знать: ни на одном языке вообще нельзя сравнивать дробные числа на равенство!

Вообще-вообще?

Ну за Матлабы всякие не говорю, но вроде не один общий язык не гарантирует что 0.1 + 0.2 == 1.2 / 4.0 в любом случае.

А если вы получаете параметр и вам нужно узнать то же самое это число или другое, чтобы понять, нужно ли пересчитывать всё, что от этого параметра зависит, то тоже сравнивать нельзя? А то после таких мощных утверждений всякие умельцы goto по-убирают, а потом оказывается, что есть алгоритмы, которые без него нормально и не запрограммируешь, и сидят с кодом, в котором хрен разберёшься. А некоторые, после громких заявлений и исключения поубирали и теперь страдают, но едят кактус и делают вид, что так оно и надо. А кто-то дженерики по-убирал. А одни даже обычный цикл по целым числами убрали и range вместо него кривыми болтами прикрутили, и только через 10 лет добавили кривую-косую оптмизицию в свой for-each.
Это я к тому, что поймал себя на мысли, что мне неприлично часто приходится сравнивать на равенство плавающие числа. Повезло вот так. Есть же ситуаций, где можно и нужно сравнивать float/double, просто нужно понимать что, зачем и как оно работает. А если программист не знает ничего о представлении плавающих чисел, то хреновый он программист, и потому всё равно найдёт из чего себе в ногу выстрелить. Ведь есть же ещё переполнения, потеря точности при математических операциях, фокусы с округлениями и приведением к целому, не-числа и бесконечности и куча всего другого интересного, что произрастает из двоичного представления чисел.

А если вы получаете параметр и вам нужно узнать то же самое это число или другое

Советую вот так:

static inline bool qFuzzyCompare(float p1, float p2)
{
    return (qAbs(p1 - p2) * 100000.f <= qMin(qAbs(p1), qAbs(p2)));
}

Это кусок Qt.

Вот так нетривиально, да и с "магическим числом" авторы Qt рассчитывают относительную погрешность в 0,001%.

Из названия функции непонятно, что это относительное, а не абсолютное сравнение. А так же оно некорректно сработает при p1 == 0.0. Ладно, если известно что сравнение будет с нулём. А если p1 или p2 получены в результате предыдущих расчётов и содержит 0.

Возможно, эти тонкости и отражены в документации. Но сравнение с нулём может получиться неожиданно. Это потребует дополнительной проверки.

Вообще, лучше делать при помощи isclose()

import math 

a = 0.1 + 0.2
b = 1.2 / 4
print(math.isclose(a, b))

так как играет роль ещё и большие это числа или маленькие.

Это-то понятно, обычно сравнивают модуль разности чисел и некую дельта 10^{-9}-10^{-20}. Напрягает то, что в этом случае дельта равна единице!

Что-то боязно стало использовать Питон.

обычно сравнивают модуль разности чисел и некую дельта

А вот неправильно делают. Так можно делать только если примерно известен порядок сравниваемых чисел. Проблема из поста актуальна практически для всех языков, которые используют нативные float. Например, go:

p := 9007199254740992.0
q := 9007199254740993.0
log.Printf("%f, %f, %t\n", p, q, p == q)

Получаем на выходе:

   9007199254740992.000000, 9007199254740992.000000, true

Я бы уточнил, что не "дробные", а "с плавающей точкой".

Не дробные, а с плавающей запятой. Числа с фиксированной запятой сравниваются без проблем.

ни на одном языке вообще нельзя сравнивать дробные числа на равенство!

Можно и нужно - но, понимая, что именно делаешь и почему. И имея в виду возможные проблемы от реализации. А вот если не понимать... вот тут целое собачье кладбище зарыто.

В обсуждаемом случае сравнение int и float - это не сравнение двух float. Это отдельный вариант (хоть внутри и получается конверсия к float (double), но она замаскирована видом констант). И последствия от него примерно такие же, как в JS ситуация типа "ваш номер карточки 4.1492103e+15". Это больше проблема типизации, чем собственно сравнение floatʼов.

Сравнение floatʼов на равенство вредно тогда, когда мы говорим о результатах вычислений с заведомо приближёнными значениями (типично для прикладной математики всех видов выше, чем 4 арифметические операции). Вот там появляется правило про корректный выбор относительной погрешности (каковой выбор ещё надо осилить, не всегда сразу получается адекватная погрешность).

А вот если значения по каким-то причинам точные (или гарантированно точные, или результат однозначно определённых операций типа i*0.01), тогда и точное сравнение возможно. Также есть случаи сравнения с нулём, как признак полной потери значения (даже не денормализованные). Но очень часто само по себе такое сравнение подсказывает, что выбрана неверная модель представления числа.

В финансовых расчётах точное сравнение возможно и нужно. Но тут вопрос, с какого момента может начаться недопустимое округление. Всякие Decimal из С# маскируют проблему до предела, давая аж до 28 знаков точности... обычно хватает. Но с фиксированной точкой таки честнее (и проблема, если что, выскочит явно).

Но попросту, конечно, можно сказать "нельзя!" и это будет как деление на ноль ;)

Плюс один вопрос на собесах

Я не очень понял при чём тут Питон. IEEE 754 флоаты имеют динамическую разрешающую способность, и начиная с какого-то достаточно большого числа машинное эпсилон (т.е. расстояние между двумя соседними флоатами сетки представления) становится больше единицы. Числа в сетке начинают идти через 2, потом через 4, потом через 8 и т.п. А обычные инты в Питоне -- biginteger, у них разрешающая способность везде равна 1. Вполне логично, что когда шаг сетки будет достаточно большой (т.е. само число достаточно большое по модулю), bigint будет представлять целые числа точнее, чем IEEE 754 float (по-крайней мере, 64-битный флоат, я не знаю, может другие представления у них с какими-то нюансами сделаны). Это должно быть так в любом языке, не только в Питоне. В чём мысль то?

Числа в сетке начинают идти через 2, потом через 4, потом через 8 и т.п. А обычные инты в Питоне -- biginteger, у них разрешающая способность везде равна 1

Именно что. Когда-то я вручную подбирал алгоритм отбрасывания лишних (незначащих) цифр при печати разных значений (потребовалось). И для удобства погружения в задачу даже для себя

картинку наглядную сделал
Особенности преобразования целых в real*4 и обратно. Пояснение - ниже в тексте. P.S. Прошу прощения, но картинка выложена в том виде, как я ее использовал. Кому такой формат не очень удобен - берите идею, и рисуйте по-своему ;-)
Особенности преобразования целых в real*4 и обратно. Пояснение - ниже в тексте.
P.S. Прошу прощения, но картинка выложена в том виде, как я ее использовал.
Кому такой формат не очень удобен - берите идею, и рисуйте по-своему ;-)

На верхнем графике (бокс 1) показан ряд натуральных чисел, заданных, как целые, и затем сохраненных в 4-байтовом Real, а на графике (5) - ряд округленных (дискретизированных до целого) приращений между последовательными значениями ряда (1). Видно, что для значений от 0 и до примерно 16 800 000 приращения равны 1, что и должно быть в идеале. Но далее до 33 600 000 приращения чередуются 0/2; далее до 67 000 000 идет чередование 0/4 и т.д. Это означает, что минимальный шаг между соседними Real равен (в целочисленном представлении) 1, 2, 4 и т.д. Другими словами, по мере роста целых значений они округляются к ближайшему Real со все большей погрешностью, которая начинает превышать шаг между integer уже начиная с 16 млн.

На графиках 2-4 и 6-8 показаны аналогичные пары для рядов вида real(N)+1.0, realN)+0.5 и realN)+0.3. Видно, что раньше всего - начиная со значений 8 400 000 - точность представления теряется для ряда вида real[целое]+0.5. 8 400 000 - это как раз тот порог, где шаг между соседними real достигает 0.5 При этом числа вида real[целое]+0.5 округляются то в одну сторону, то в другую.

И то же самое в числах: если число равно XXX, то шаг между соседними представимыми real*4-числами равен (с округлением) YYY:
XXX YYY
16 800 000 2
1 680 000 0.2
168 000 0.02
16 800 0.002
1 680 0.0002
168 0.000 02
16.8 0.000 0002
1.68 0.000 000 02
0.168 0.000 000 002

Если число равно XXX, то ближайшее real-представимое значение
отличается на YYY. Чтобы точность десятичной записи соответствовала
точности машинного представления real-чисел, в записи должно быть
NNN знаков после десятичной точки:
XXX YYY NNN
8 400 000 1 0
840 000 0.1 1
84 000 0.01 2
8 400 0.001 3
840 0.0001 4
84 0.00001 5
8.4 0.000001 6
0.84 0.0000001 7
0.084 0.00000001 8

UPD: что-то насчет графика 6 сомнения у меня возникли сейчас - точно ли там real(N)+1.0. В в моем readme к картинке вроде бы так, но могла опечатка закрасться. Давно уже очень дело было, а пересчитывать сейчас некогда.

Я когда читал, не заметил точку в конце второго числа, поэтому удивился, что два одинаковых BigInt при сравнении дают false. В реальности же в конце второго числа стоит точка, из-за чего оно хранится как float. Получается сравнение 9007199254740993 == 9007199254740992.0, что логично даёт false.

Не стоит сравнивать float числа с помощью оператора ==. Можно сравнивать так или использовать модуль decimal:

num = 0.1 + 0.1 + 0.1
eps = 0.000000001           # точность сравнения
if abs(num - 0.3) < eps:    # число num отличается от числа 0.3 менее чем 0.000000001
    print('YES')
else:
    print('NO')

откровения западных коллег иногда на уровне "ой, солнышко вышло из-за гор, папа, а это почему?" Хотя судя по имени ABHINAV UPADHYAY - это скорее индусский первооткрыватель.

Самое интересное начинается при регистровой оптимизации. Регистры FPU в x86-64 80-битные. А числа с плавающей запятой в памяти - 64 битные. Поэтому сравнение (a*b)==c может оказаться истинным или ложным, в зависимости от того, сохранялся ли результат произведения в память, или оставался в регистре FPU.

Великолепная статья! Мне больше повезло, так как программа была намного проще Вашей и разница в ассемблерном коде сразу бросилась в глаза. Тоже выкрутился volatile.

Еперный бабай, так он же точку в конце вторых чисел поставил)

Подожди как, но ведь числа из начала статьи выглядят как целое, без точек и дробной части.

В Python целочисленное значение сравнивается с представлением числа с плавающей точкой

Знаете, наверно программистов разбаловали, но вообще хотелось бы, чтобы целочисленное (БЕЗ явно назначенной конвертации в плавающую точку) всегда равнялось самому себе, без условностей, что где-то там, под капотом, язык как-то по-другому себе это представляет и потому... К числам с плавающей точкой вопросов нет.

>Знаете, наверно программистов разбаловали, но вообще хотелось бы, чтобы целочисленное (БЕЗ явно назначенной конвертации в плавающую точку) всегда равнялось самому себе, без условностей, что где-то там, под капотом, язык как-то по-другому себе это представляет и потому... 

Так там же вторая половина оканчивается на .0 что и равносильно явно конвертации во флоат

Оканчивается на «.», а не «.0». Числа длинные и «xxxxx.0» было бы гораздо заметнее, чем просто «xxxxx.» Потому сразу и незаметно было, это уже сильно позже присмотрелся. Спасибо)

Так то да, тогда всё сходится. Ни при каких обстоятельствах никие плавающие напрямую сравнивать нельзя.

В текущем python наоборот всё корректно сделано в плане арифметики. Например, целочисленное деление отрицательного целого числа на положительное с остатком даёт корректный остаток - неотрицательный, в отличие например от С++.

То, что сравнивается bigint целое с числом с "точкой" ограниченной разрядности, это само по себе некорректно. Если ты поставил точку после числа, то оно - число с плавающей точкой. В С++ аналогично, только там целые по умолчанию ограничены по разрядности.

Про внутренности интересно было почитать, в принципе, но в итоге слишком много букв непонятно про что. Так и непонятно - что именно здесь связано с чем-то там уникальным в python? Сравниваются целые с float, казалось бы, всё понятно откуда такие результаты и почему так делать нельзя.

В MicroPython не воспроизводится. Исходный пример печатает три раза True.

Sign up to leave a comment.

Articles