Почему значение с плавающей запятой 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')
4 152

4 ответа:

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

можно использовать .hex метод в Python для просмотра внутреннего представления числа (в основном,точно двоичное значение с плавающей запятой, а не приближение base-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 is 0х1.999999999999a раз 2^-4. "А" в конце означает цифру 10 - другими словами, 0.1 в двоичной плавающей запятой очень немного больше, чем" точное " значение 0.1 (потому что окончательный 0x0.99 округляется до 0x0.ля.) Когда вы умножаете это на 4, степень двух, экспонента сдвигается вверх (от 2^-4 до 2^-2), но число в остальном остается неизменным, поэтому 4*0.1 == 0.4.

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

Python 3's float repr предназначен для round-trippable, то есть показанное значение должно быть точно конвертировано в исходное значение. Поэтому он не может отображать 0.3 и 0.1*3 точно так же, или два разные числа в конечном итоге то же самое после кругового отключения. Следовательно, Python 3's repr двигатель выбирает для отображения один с небольшой очевидной ошибкой.

reprstr в Python 3) выведет столько цифр, сколько требуется, чтобы сделать значение однозначным. В этом случае результат умножения 3*0.1 не самое близкое значение к 0.3 (0x1.3333333333333p-2 в шестнадцатеричном формате), это на самом деле один LSB выше (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')

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

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

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

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

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

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

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

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

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

поскольку плавающая точка находится в научной нотации, любое умножение на степень основания влияет только на экспоненциальную часть числа. Например, 1.231 e+2 * 100 = 1.231 e+4 для десятичной системы счисления, а также 1. 00101010e11 * 100 = 1.00101010e101 в двоичной системе счисления. Если я умножу на немощность основания, значащие цифры также будут затронуты. Например 1.2Е1 * 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 последние несколько значимых цифр, вероятно, будут отличаться от деления поплавка 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.

нет допуска к потере точности, если поплавок x (0.3) точно не равен поплавку y (0.1*3), то repr(x) не совсем равно repr (y).