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

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

Но я не понимаю, почему 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')

Ответ 1

Простой ответ заключается в том, что 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 float repr предназначен для округления, т.е. показанное значение должно быть в точности конвертировано в исходное значение. Поэтому он не может отображать 0.3 и 0.1*3 точно так же, или два разных номера будут совпадать после кругового отключения. Следовательно, движок Python 3 repr выбирает отображение с небольшой видимой ошибкой.

Ответ 2

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')

Ответ 3

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

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

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

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

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

Ответ 4

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

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

Обратное к любому числу, которое имеет простой коэффициент числа, не разделяемый базой, всегда будет приводить к повторному представлению точечной точки. Например, 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 и т.д. Он также может иметь магические шаблоны сигфига для других комбинаций, таких как поплавок 3, деленный на поплавок 10 и другие магические узоры статистически, вероятно, будут сформированы путем деления на 10.

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

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

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

Не существует толерантности к прецизионным потерям, если float x (0.3) не точно равен float y (0.1 * 3), тогда repr (x) не точно равно repr (y).