Почему значение с плавающей точкой 4 * 0.1 выглядит хорошо в Python 3, а 3 * 0.1 - нет?

158

Я знаю, что большинство десятичных чисел не имеют точного представления с плавающей запятой (математика с плавающей запятой не работает? ).

Но я не понимаю , почему 4*0.1печатается хорошо , как 0.4, но 3*0.1это не так , когда оба значения фактически имеют уродливые десятичные представления:

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Айвар
источник
7
Потому что некоторые числа могут быть представлены точно, а некоторые нет.
Морган Трепп
58
@MorganThrapp: нет, это не так. ОП спрашивает о довольно произвольном выборе форматирования. Ни 0.3, ни 0.4 не могут быть представлены точно в двоичной форме с плавающей точкой.
Вирсавия
42
@BartoszKP: Прочитав документ несколько раз, это не объясняет , почему Python отображает , 0.3000000000000000444089209850062616169452667236328125как 0.30000000000000004и , 0.40000000000000002220446049250313080847263336181640625как .4даже если они появляются , чтобы иметь такую же точность, и , таким образом , не дает ответа на вопрос.
Утка
6
См. Также stackoverflow.com/questions/28935257/… - Я несколько раздражен тем, что он был закрыт как дубликат, а этот нет.
Random832,
12
Повторно открыт, пожалуйста, не закрывайте его, так как дубликат "математика с плавающей точкой не работает" .
Антти Хаапала

Ответы:

301

Простой ответ заключается в том, что 3*0.1 != 0.3из-за ошибки квантования (округления) (тогда 4*0.1 == 0.4как умножение на степень два обычно является «точной» операцией).

Вы можете использовать .hexметод в Python для просмотра внутреннего представления числа (в основном, точное двоичное значение с плавающей запятой, а не приближение по основанию-10). Это может помочь объяснить, что происходит под капотом.

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0,1 - это 0x1,999999999999a, умноженное на 2 ^ -4. «A» в конце означает цифру 10 - другими словами, 0,1 в двоичной переменной с плавающей запятой очень немного больше, чем «точное» значение 0,1 (потому что конечное 0x0,99 округляется до 0x0.a). Когда вы умножаете это на 4, степень двойки, показатель степени смещается вверх (с 2 ^ -4 до 2 ^ -2), но в остальном число остается неизменным, поэтому 4*0.1 == 0.4.

Однако при умножении на 3 небольшая маленькая разница между 0x0,99 и 0x0.a0 (0x0,07) увеличивается до ошибки 0x0,15, которая отображается как ошибка с одной цифрой в последней позиции. Это приводит к тому, что 0,1 * 3 будет немного больше округленного значения 0,3.

Число с плавающей запятой в Python 3 reprрассчитано на круговое переключение , то есть отображаемое значение должно быть точно преобразовано в исходное значение. Таким образом, он не может отображаться 0.3и 0.1*3точно так же, иначе два разных числа будут одинаковыми после кругового отключения. Следовательно, reprдвижок Python 3 выбирает для отображения один с небольшой очевидной ошибкой.

nneonneo
источник
25
Это удивительно полный ответ, спасибо. (В частности, спасибо за показ .hex(); я не знал, что он существовал.)
NPE
21
@supercat: Python пытается найти самую короткую строку, которая бы округлилась до желаемого значения , каким бы оно ни было. Очевидно, что оцененное значение должно быть в пределах 0,5 муль (или оно округляется до чего-то еще), но в неоднозначных случаях может потребоваться больше цифр. Код очень грубый, но если вы хотите взглянуть на него: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo
2
@supercat: всегда самая короткая строка, которая находится в пределах 0,5 ulp. ( Строго внутри, если мы смотрим на число с плавающей точкой с нечетным младшим битом; т. Е. Самая короткая строка, которая заставляет его работать с круглыми связями с четными) Любые исключения из этого являются ошибкой и должны быть сообщены.
Марк Дикинсон
7
@MarkRansom Конечно, они использовали что-то еще, eпотому что это уже шестнадцатеричная цифра. Может быть, pдля власти, а не экспоненты .
Берги
11
@Bergi: Использование pв этом контексте восходит (по крайней мере) к C99, а также появляется в IEEE 754 и на других языках (включая Java). Когда float.hexи float.fromhexбыли реализованы (мной :-), Python просто копировал то, что к тому времени стало практикой. Я не знаю, было ли намерение «p» для «Силы», но это кажется хорошим способом обдумать это.
Марк Дикинсон
75

reprstrв Python 3) выдаст столько цифр, сколько требуется, чтобы сделать значение однозначным. В этом случае результат умножения3*0.1 не является ближайшим значением к 0,3 (0x1,3333333333333p-2 в шестнадцатеричном формате), это фактически на один младший бит выше (0x1,3333333333334p-2), поэтому ему нужно больше цифр, чтобы отличить его от 0,3.

С другой стороны, умножение 4*0.1 делает получить наиболее близкое значение 0,4 (0x1.999999999999ap-2 в шестнадцатеричной форме ), так что не требуется никаких дополнительных цифр.

Вы можете проверить это довольно легко:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

Я использовал шестнадцатеричное обозначение выше, потому что это красиво и компактно и показывает разницу в битах между двумя значениями. Вы можете сделать это самостоятельно, например (3*0.1).hex(). Если вы предпочитаете видеть их во всей их десятичной славе, вот вам:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Марк Рэнсом
источник
2
(+1) Хороший ответ, спасибо. Как вы думаете, возможно, стоит проиллюстрировать точку «не ближайшей ценности», включив результат 3*0.1 == 0.3и 4*0.1 == 0.4?
NPE
@NPE Я должен был сделать это прямо из ворот, спасибо за предложение.
Марк Рэнсом
Интересно, стоит ли отмечать точные десятичные значения ближайших «двойников» до 0,1, 0,3 и 0,4, так как многие люди не могут читать шестнадцатеричные числа с плавающей точкой.
суперкат
@supercat Вы делаете хорошую мысль. Помещение этих сверхбольших двойников в текст будет отвлекать, но я подумал, как добавить их.
Марк Рэнсом
25

Вот упрощенный вывод из других ответов.

Если вы проверяете float в командной строке Python или печатаете его, он проходит через функцию repr которая создает его строковое представление.

Начиная с версии 3.2, Python strиrepr использовать сложную схему округления, которая предпочитает симпатичной десятичные , если это возможно, но использует больше цифр , где необходимо гарантировать Биективные (один к одному) отображение между поплавками и их строковые представления.

Эта схема гарантирует, что значение repr(float(s))выглядит хорошо для простых десятичных чисел, даже если они не могут быть представлены точно как числа с плавающей запятой (например, когда s = "0.1").

В то же время это гарантирует, что float(repr(x)) == xимеет место для каждого поплавкаx

Айвар
источник
2
Ваш ответ точен для версий Python> = 3.2, где strи reprодинаковы для чисел с плавающей точкой. Для Python 2.7 reprимеет свойства, которые вы идентифицируете, но strгораздо проще - он просто вычисляет 12 значащих цифр и создает на их основе строку вывода. Для Python <= 2,6 оба reprи strоснованы на фиксированном количестве значащих цифр (17 для repr, 12 для str). (И никому нет дела до Python 3.0 или Python 3.1 :-)
Марк Дикинсон
Спасибо @MarkDickinson! Я включил ваш комментарий в ответ.
Айвар
2
Обратите внимание, что округление от оболочки происходит из- reprза того, что поведение Python 2.7 будет идентичным ...
Антти Хаапала,
5

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

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

Обращение любого числа, имеющего множитель простого числа, которое не используется совместно с основанием, всегда приводит к повторяющемуся представлению точки. Например, 1/7 имеет главный множитель 7, который не используется совместно с 10, и, следовательно, имеет повторяющееся десятичное представление, и то же самое верно для 1/10 с простыми множителями 2 и 5, причем последний не делится с 2 ; это означает, что 0.1 не может быть точно представлено конечным числом битов после точки точки.

Поскольку 0.1 не имеет точного представления, функция, которая преобразует аппроксимацию в строку десятичной точки, обычно пытается аппроксимировать определенные значения, чтобы они не получали неинтуитивных результатов, таких как 0.1000000000004121.

Так как с плавающей запятой находится в научной нотации, любое умножение на степень основания влияет только на экспонентную часть числа. Например, 1.231e + 2 * 100 = 1.231e + 4 для десятичной записи, а также 1.00101010e11 * 100 = 1.00101010e101 в двоичной записи. Если я умножу на не-мощность базы, значимые цифры также будут затронуты. Например, 1.2e1 * 3 = 3.6e1

В зависимости от используемого алгоритма он может попытаться угадать общие десятичные дроби, основываясь только на значащих цифрах. И 0.1, и 0.4 имеют одинаковые значащие цифры в двоичном виде, потому что их числа с плавающей точкой по сути являются усечениями (8/5) (2 ^ -4) и (8/5) (2 ^ -6) соответственно. Если алгоритм идентифицирует шаблон 8/5 sigfig как десятичное число 1.6, то он будет работать с 0.1, 0.2, 0.4, 0.8 и т. Д. Он может также иметь магические шаблоны sigfig для других комбинаций, таких как число с плавающей точкой 3, деленное на число с плавающей точкой 10 и другие магические паттерны, статистически вероятные при делении на 10.

В случае 3 * 0,1 последние несколько значащих цифр, вероятно, будут отличаться от деления числа с плавающей точкой на число с плавающей точкой 10, что приведет к тому, что алгоритм не сможет распознать магическое число для константы 0,3 в зависимости от его допуска к потере точности.

Изменить: https://docs.python.org/3.1/tutorial/floatingpoint.html

Интересно, что существует много разных десятичных чисел, которые имеют одну и ту же ближайшую приблизительную двоичную дробь. Например, числа 0,1 и 0,10000000000000001 и 0,1000000000000000055511151231257827021181583404541015625 все приблизительно равны 3602879701896397/2 ** 55. Поскольку все эти десятичные значения имеют одинаковое приближение, любое из них может отображаться, сохраняя при этом инвариантный x (сохраняющий инвариантный e) ) == х.

Не допускается потеря точности, если значение с плавающей запятой x (0.3) не равно точно числу с плавающей запятой y (0,1 * 3), тогда repr (x) точно не равно repr (y).

AkariAkaori
источник
4
Это на самом деле не добавляет много к существующим ответам.
Антти Хаапала
1
«В зависимости от используемого алгоритма он может попытаться угадать общие десятичные дроби только на основе значащих цифр». <- Это похоже на чистую спекуляцию. Другие ответы описывают, что на самом деле делает Python .
Марк Дикинсон