Почему 1 // 0.01 == 99 в Python?

31

Я полагаю, что это классический вопрос о точности с плавающей запятой, но я пытаюсь обдумать этот результат, работая 1//0.01в Python 3.7.5 99.

Я предполагаю, что это ожидаемый результат, но есть ли способ решить, когда его безопаснее использовать int(1/f), чем 1//f?

Альберт Джеймс Тедди
источник
4
Да, всегда безопаснее int (1 / f). Просто потому, что // это раздел FLOOR, и вы ошибочно считаете его РАУНДОМ.
Perdi Estaquel
4
Возможен ли дубликат математики с плавающей запятой?
pppery
2
Не дубликат Это может работать, как и ожидалось, 99,99%, всегда используя round()и никогда //или int(). Связанный вопрос о сравнении с плавающей запятой не имеет ничего общего с усечением и не так легко исправить.
Maxy

Ответы:

23

Если бы это было деление с действительными числами, 1//0.01было бы ровно 100. Поскольку они являются аппроксимациями с плавающей запятой, тем не менее, они 0.01немного больше, чем 1/100, что означает, что частное немного меньше, чем 100. Именно это значение 99. что-то затем вычисляется до 99.

chepner
источник
3
Это не относится к части «есть ли способ решить, когда это безопаснее».
Скотт Хантер
10
«Безопаснее» не очень четко определено.
chepner
1
Достаточно, чтобы полностью игнорировать это, особенно когда ОП знает о проблемах с плавающей запятой?
Скотт Хантер
3
@chepner Если «безопаснее» недостаточно четко определено, то, возможно, лучше попросить разъяснений: /
2
мне совершенно ясно, что «безопаснее» означает «ошибка не хуже дешевого карманного калькулятора»
maxy
9

Причины такого исхода такие же, как вы утверждаете, и объяснены в « Математика с плавающей запятой нарушена? и многие другие подобные вопросы и ответы.

Когда вы знаете количество десятичных чисел числителя и знаменателя, более надежный способ - сначала умножить эти числа, чтобы они могли рассматриваться как целые числа, а затем выполнить целочисленное деление для них:

Так что в вашем случае 1//0.01следует сначала преобразовать в 1*100//(0.01*100)100.

В более экстремальных случаях вы все равно можете получить «неожиданные» результаты. Может потребоваться добавить roundвызов к числителю и знаменателю перед выполнением целочисленного деления:

1 * 100000000000 // round(0.00000000001 * 100000000000)

Но, если речь идет о работе с фиксированными десятичными знаками (деньги, центы), тогда рассмотрите возможность работы с центами как единицей , так что вся арифметика может быть выполнена как целочисленная арифметика, и конвертировать только в / из основной денежной единицы (доллар) при выполнении I / O.

Или, в качестве альтернативы, используйте библиотеку для десятичных чисел, например, десятичную , которая:

... обеспечивает поддержку быстрой правильно округленной десятичной арифметики с плавающей точкой.

from decimal import Decimal
cent = Decimal(1) / Decimal(100) # Contrary to floating point, this is exactly 0.01
print (Decimal(1) // cent) # 100
trincot
источник
3
«который, очевидно, равен 100». Не обязательно: если 0,01 не является точным, то 0,01 * 100 не так хорошо. Он должен быть «настроен» вручную.
glglgl
8

Вы должны принять во внимание, что //это floorоператор, и поэтому вы должны сначала подумать, что у вас есть равная вероятность упасть в 100, как в 99 (*) (потому что операция будет 100 ± epsilonпри epsilon>0условии, что шансы получить точно 100,00 ..0 очень низкие.)

Вы можете увидеть то же самое со знаком минус,

>>> 1//.01
99.0
>>> -1//.01
-100.0

и вы должны быть удивлены.

С другой стороны, int(-1/.01)сначала выполняется деление, а затем применяется int()число, которое является не полом, а усечением до 0 ! Это означает, что в этом случае

>>> 1/.01
100.0
>>> -1/.01
-100.0

следовательно,

>>> int(1/.01)
100
>>> int(-1/.01)
-100

Округление, однако, даст ВАШ ожидаемый результат для этого оператора, потому что, опять же, ошибка для этих цифр мала.

(*) Я не говорю, что вероятность одинакова, я просто говорю, что априори, когда вы выполняете такое вычисление с плавающей арифметикой, которая является оценкой того, что вы получаете.

MyRadio
источник
7

Числа с плавающей запятой не могут точно представлять большинство десятичных чисел, поэтому при вводе литерала с плавающей запятой вы фактически получаете приближение этого литерала. Аппроксимация может быть больше или меньше, чем введенное число.

Вы можете увидеть точное значение числа с плавающей запятой, приведя его к десятичному или дробному числу.

>>> from decimal import Decimal
>>> Decimal(0.01)
Decimal('0.01000000000000000020816681711721685132943093776702880859375')
>>> from fractions import Fractio
>>> Fraction(0.01)
Fraction(5764607523034235, 576460752303423488) 

Мы можем использовать тип Fraction, чтобы найти ошибку, вызванную нашим неточным литералом.

>>> float((Fraction(1)/Fraction(0.01)) - 100)
-2.0816681711721685e-15

Мы также можем узнать, как гранулированные числа с плавающей запятой двойной точности около 100, используя nextafter от numpy.

>>> from numpy import nextafter
>>> nextafter(100,0)-100
-1.4210854715202004e-14

Исходя из этого, мы можем предположить, что ближайшее число с плавающей запятой 1/0.01000000000000000020816681711721685132943093776702880859375фактически равно 100.

Разница между 1//0.01и int(1/0.01)заключается в округлении. 1 // 0.01 округляет точный результат до следующего целого числа за один шаг. Таким образом, мы получаем результат 99.

int (1 / 0.01), с другой стороны, округляет в два этапа, сначала округляет результат до ближайшего числа с плавающей запятой двойной точности (которое точно равно 100), затем округляет это число с плавающей запятой до следующего целого числа (которое является опять ровно 100).

plugwash
источник
Назвать это просто округлением вводит в заблуждение. Его следует называть усечением или округлением до нуля : int(0.9) == 0аint(-0.9) == 0
maxy
Это двоичные типы с плавающей запятой, о которых вы здесь говорите. (Также есть десятичные типы с плавающей запятой.)
Стивен С.
3

Если вы выполните следующее

from decimal import *

num = Decimal(1) / Decimal(0.01)
print(num)

Выход будет:

99.99999999999999791833182883

Это то, как оно представлено внутри, поэтому его округление //даст99

дождь
источник
2
Это достаточно точно, чтобы показать ошибку в этом случае, но имейте в виду, что арифметика "Десятичная" также не является точной.
штепсельная розетка
С Decimal(0.01)вами слишком поздно, ошибка уже закралась, прежде чем позвонить Decimal. Я не уверен, как это ответ на вопрос ... Сначала вы должны рассчитать точный 0,01 с Decimal(1) / Decimal(100), как я показал в своем ответе.
трико
@trincot Мой ответ на вопрос в заголовке «Почему 1 // 0,01 == 99» Я пытался показать ОП, как плавающие числа обрабатываются внутри.
дождь