0 0111111101 11001100110011001100110011001100110011001100110011010.
Где:
•
0
обозначает знак +
(и 1
обозначает -
)•
0111111101
обозначает exponent, равную 0^10 + 2^9 + 2^8 + 2^7 + 2^6 + итд = 1019
. Вычтем 1023 (размерность double) и получим итоговое значение: 1019 - 1023 = 4•
11001100110011001100110011001100110011001100110011010
обозначет "significand" или "мантису", которая равна: 2^-exp + 2^-exp-1 + 2^-exp-2 + итд ~= 0.1
Вот так мы можем примерно представить 0.1 в виде float. Примерно – потому что все вычисления идут с погрешностью. Мы можем проверить данное утверждение, добавив погрешность вручную:
>>> assert 0.1 + 2.220446049250313e-18 == 0.1
Значение внешне не изменилось при добавлении погрешности. Посмотрим на sys.float_info.epsilon, который устанавливает необходимый порог для минимальных отличий 1.0 от следующего float числа.
>>> import sys
>>> sys.float_info.epsilon
2.220446049250313e-16
>>> assert 1.0 + sys.float_info.epsilon > 1.0
>>> assert 1.0 + 2.220446049250313e-17 == 1.0 # число меньше epsilon
Как конкретно будет выглядеть
0.1
? А вот тут нам уже поможет Decimal
для отображения полного числа в десятичной системе:>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
И вот ответ про 0.1 + 0.2, полное демо с битиками:
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> decimal.Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')
>>> decimal.Decimal(0.1 + 0.2)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> decimal.Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
Числа не равны друг другу, потому что их разница больше предельной точности float. А сам Decimal может использовать любую точность под задачу.
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 6
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
>>> getcontext().prec = 28
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
Но и
Decimal
не может в абсолютную точность, потому что есть в целом невыразимые в десятичной системе числа, такие как math.pi
, ⅓
, тд. С чем-то из них может помочь fractions.Fraction
для большей точности, но от существования иррациональных чисел никуда не деться.Почему всем норм, что у нас с
float
такие погрешности в вычислениях? Потому что во многих задачах абсолютная точность недостижима и не имеет смысла. Благодаря плавающей точке мы можем хранить как очень большие, так и очень маленькие числа без существенных затрат памяти. А ещё float - очень быстрый. В том числе за счет аппаратного ускорения.» pyperf timeit -s 'a = 0.1; b = 0.2' 'a + b'
.....................
Mean +- std dev: 8.75 ns +- 0.2 ns
» pyperf timeit -s 'import decimal; a = decimal.Decimal("0.1"); b = decimal.Decimal("0.2")' 'a + b'
.....................
Mean +- std dev: 27.7 ns +- 0.1 ns
Разница в 3 раза.
Про то, как устроен float внутри – рассказывать не буду. У Никиты Соболева недавно было большое и подробное видео на тему внутреннего устройства float. У него действительно хороший технический контент, советую подписаться: @opensource_findings
Итого
Если у вас нет требований по работе именно с десятичной записью числа (как, например, в бухгалтерии), то используйте float. Он даст достаточную точность и хорошую скорость. Если вы хотите, чтобы расчеты велись в десятичных цифрах и ваши расчеты построены так, что абсолютная точность достижима, то используйте Decimal.
Дополнительные материалы:
• https://www.youtube.com/@sobolevn/
• https://0.30000000000000004.com
• https://en.wikipedia.org/wiki/X87
• http://aco.ifmo.ru/el_books/numerical_methods/lectures/app_1.html