TG Telegram Group & Channel
sanspie's notes | United States America (US)
Create: Update:

float и Decimal

Вас никогда не удивляло, что 0.1 + 0.2 != 0.3? Почему float считает с погрешностями, и всем норм?

Дело в том, что 0.1 выглядит как

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

float и Decimal

Вас никогда не удивляло, что 0.1 + 0.2 != 0.3? Почему float считает с погрешностями, и всем норм?

Дело в том, что 0.1 выглядит как

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
👍41


>>Click here to continue<<

sanspie's notes




Share with your best friend
VIEW MORE

United States America Popular Telegram Group (US)