Сообщить об ошибке.

Арифметика с типом float в Python: проблемы и ограничения

Числа с плавающей точкой представлены в компьютерах в виде двоичных дробей. Например, десятичная дробь 0.125 имеет значение 1/10 + 2/100 + 5/1000, и таким же образом двоичная дробь 0.001 имеет значение 0/2 + 0/4 + 1/8. Эти две дроби имеют одинаковые значения, единственное реальное отличие состоит в том, что первая записана в дробной записи с основанием 10, а вторая - с основанием 2.

К сожалению, большинство десятичных дробей не могут быть представлены точно как двоичные дроби. Следствием этого является то, что, как правило, вводимые десятичные числа с плавающей запятой аппроксимируются только двоичными числами с плавающей запятой, фактически сохраненными в машине.

Сначала проблему легче понять из базы 10. Рассмотрим дробь 1/3. Вы можете приблизить это как основную 10 фракцию: 0.3 или лучше, 0.33или лучше, 0.333 и так далее. Независимо от того, сколько цифр вы хотите записать, результат никогда не будет ровно 1/3, но будет все более приближенным к 1/3.

Таким же образом, независимо от того, сколько цифр из 2-х оснований вы хотите использовать, десятичное значение 0,1 не может быть представлено в точности как дробь из 2-х оснований. В базе 2 1/10 - бесконечно повторяющаяся дробь 0.0001100110011001100110011...

Остановитесь на любом конечном количестве битов, и вы получите приближение. На большинстве современных машин числа с плавающей запятой аппроксимируются с использованием двоичной дроби, а числитель использует первые 53 бита, начиная с самого старшего бита, а знаменатель - как степень двух. В случае 1/10 двоичная дробь равна 3602879701896397/2 ** 55, что близко, но не точно равно истинному значению 1/10.

Многие пользователи не знают о приближении из-за способа отображения значений. Python печатает только десятичное приближение к истинному десятичному значению двоичного приближения, хранящегося на машине. На большинстве машин, если бы Python должен был печатать истинное десятичное значение двоичного приближения, хранящегося для 0.1, он должен был бы отображать

>>> 0.1
# 0.1000000000000000055511151231257827021181583404541015625

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

>>> 1 / 10
# 0.1

Просто помните, что хотя напечатанный результат выглядит как точное значение 1/10, фактическое сохраненное значение является ближайшей представимой двоичной дробью.

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

Исторически Python и встроенная функция repr() выбирали функцию с 17 значащими цифрами, 0.10000000000000001. Начиная с Python 3.1 в большинстве систем теперь может выбирать самый короткий из них и просто отображать 0.1.

Обратите внимание, что это по своей природе двоичное число с плавающей точкой: это не ошибка в Python и не ошибка в вашем коде. Вы увидите то же самое на всех языках, которые поддерживают арифметику с плавающей запятой.

Для более приятного вывода вы можете использовать форматирование строки для получения ограниченного числа значащих цифр:

>>> import math
>>> format(math.pi, '.12g')
# '3.14159265359'
>>> format(math.pi, '.2f')
# '3.14'
>>> repr(math.pi)
# '3.141592653589793'

Важно понимать, что в действительности это иллюзия: вы просто округляете отображение истинного значения.

Одна иллюзия может породить другую. Например, поскольку 0,1 не является точно 1/10, суммирование трех значений 0,1 может также не дать точно 0,3:

>>> 0.1 + 0.1 + 0.1 == 0.3
# False

Кроме того, поскольку 0,1 не может приблизиться к точному значению 1/10, а 0,3 не может приблизиться к точному значению 3/10, предварительное округление функцией round() может не помочь:

>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
# False

Двоичная арифметика с плавающей точкой содержит много сюрпризов, подобных этому. Проблема с 0.1 подробно объясняется в разделе "Ошибка представления". Смотрите также "Опасности с плавающей точкой" для более полного описания других распространенных сюрпризов.

Как говорится, "простых ответов нет". Тем не менее, не следует чрезмерно опасаться чисел с плавающей запятой! Ошибки в операциях с плавающей запятой Python наследуются от аппаратного обеспечения чисел с плавающей запятой, и на большинстве машин они имеют порядок не более одной части в 2 ** 53 на операцию. Это более чем достаточно для большинства задач, но вам нужно помнить, что это не десятичная арифметика и что каждая операция с плавающей запятой может подвергаться новой ошибке округления.

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

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

Другая форма точной арифметики поддерживается модулем fractions, который реализует арифметику, основанную на рациональных числах, поэтому числа, такие как 1/3 могут быть представлены точно.

Если вы большой пользователь операций с плавающей запятой, вам следует взглянуть на пакет Numeric Python и многие другие пакеты для математических и статистических операций, предоставляемых проектом SciPy.

Python предоставляет инструменты, которые могут помочь в тех редких случаях, когда вы действительно хотите узнать точное значение числа с плавающей точкой. Метод float.as_integer_ratio() выражает значение типа float в виде дроби:

>>> x = 3.14159
>>> x.as_integer_ratio()
# (3537115888337719, 1125899906842624)

Поскольку отношение является точным, оно может быть использовано для воссоздания исходного значения без потерь:

>>> x == 3537115888337719 / 1125899906842624
# True

Метод float.hex() выражает число с плавающей запятой в шестнадцатеричном формате (основание 16), снова давая точное значение, сохраненное компьютером:

>>> x.hex()
# '0x1.921f9f01b866ep+1'

Это точное шестнадцатеричное представление может быть использовано для точного восстановления значения с плавающей точкой:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

Поскольку представление является точным, оно полезно для надежного переноса значений между различными версиями Python и обмена данными с другими языками, поддерживающими тот же формат, например Java.

Другим полезным инструментом является функция math.fsum(), которая помогает уменьшить потерю точности во время суммирования. Она отслеживает "потерянные цифры", когда значения добавляются в промежуточный итог. Это может повлиять на общую точность, так что ошибки не накапливаются до такой степени, что бы влиять на итоговую сумму:

>>> import math
>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

Ошибка представления.

В этом разделе подробно объясняется пример 0.1 и показано, как вы можете выполнить точный анализ подобных случаев самостоятельно. Предполагается базовое знакомство с двоичным представлением чисел с плавающей точкой.

Ошибка представления относится к тому факту, что фактически большинство десятичных дробей не могут быть представлены точно как двоичные дроби (основание 2) . Это главная причина, почему Python или Perl, C, C++, Java, Fortran и многие другие языки часто не будут отображать точное десятичное число, которое ожидаете.

Это почему? 1/10 не совсем представимо в виде двоичной дроби. Почти все машины сегодня используют арифметику IEEE-754 с плавающей точкой, и почти все платформы отображают плавающие значения Python в IEEE-754 с "двойной точностью". При вводе, компьютер стремится преобразовать 0,1 в ближайшую дробь по форме J / 2 ** N, где J - целое число, содержащее ровно 53 бита.

Перепишем 1 / 10 ~= J / (2**N) как J ~= 2**N / 10 и напоминая, что J имеет ровно 53 бита, это >= 2**52, но < 2**53, наилучшее значение для N равно 56:

>>> 2**52 <=  2**56 // 10  < 2**53
# True

То есть 56 - единственное значение для N, которое оставляет J точно с 53 битами. Наилучшее возможное значение для J тогда будет округлено:

>>> q, r = divmod(2**56, 10)
>>> r
# 6

Поскольку остаток больше половины от 10, наилучшее приближение получается округлением вверх:

>>> q+1
# 7205759403792794

Поэтому наилучшее возможное приближение к 1/10 при двойной точности 754: 7205759403792794 / 2 ** 56. Деление числителя и знаменателя на два уменьшает дробь до: 3602879701896397 / 2 ** 55

Обратите внимание, поскольку мы округлили вверх, то значение на самом деле немного больше, чем 1/10. Если бы мы не округлили, то значение был бы немного меньше 1/10. Но ни в коем случае это не может быть ровно 1/10!

Таким образом, компьютер никогда не "видит" 1/10: то, что он видит, это точная дробь, указанная выше, наилучшее двоичное приближение 754, которое он может получить:

>>> 0.1 * 2 ** 55
# 3602879701896397.0

Если мы умножим эту дробь на 10 ** 55, мы увидим значение до 55 десятичных цифр:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
# 1000000000000000055511151231257827021181583404541015625

Это означает, что точное число, хранящееся в компьютере, равно десятичному значению полученному выше. Вместо отображения полного десятичного значения многие языки, включая более старые версии Python округляют результат до 17 значащих цифр:

>>> format(0.1, '.17f')
'0.10000000000000001'

Модули fractions and decimal упрощают эти вычисления:

>>> from decimal import Decimal
>>> from fractions import Fraction
>>> Fraction.from_float(0.1)
# Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
# (3602879701896397, 36028797018963968)
>>> Decimal.from_float(0.1)
# Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> format(Decimal.from_float(0.1), '.17')
# '0.10000000000000001'