Почему значение с плавающей запятой 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 ответа:
простой ответ заключается в том, что
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'srepr
двигатель выбирает для отображения один с небольшой очевидной ошибкой.
repr
(иstr
в 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).