Содержание страницы
14. Арифметика с плавающей запятой: проблемы и ограничения¶Floating Point Arithmetic: Issues and Limitations
Числа с плавающей запятой представляются в компьютерном оборудовании в виде двоичных дробей (по основанию 2). Например, десятичная дробь
0.125
имеет значение 1/10 + 2/100 + 5/1000, и точно так же двоичная дробь
0.001
имеет значение 0/2 + 0/4 + 1/8. Эти две дроби имеют одинаковые значения, единственное различие в том, что первая записана в десятичной системе счисления, а вторая – в двоичной.
К сожалению, большинство десятичных дробей не могут быть точно представлены в виде двоичных дробей. Следствием этого является то, что, вообще говоря, вводимые десятичные числа с плавающей запятой лишь приблизительно соответствуют двоичным числам с плавающей запятой, которые на самом деле хранятся в машине.
Проблему легче сначала понять в десятичной системе. Рассмотрим дробь 1/3. Её можно приблизить десятичной дробью:
0.3
или, лучше,
0.33
или, лучше,
0.333
и так далее. Сколько бы цифр вы ни захотели записать, результат никогда не будет точно равен 1/3, но будет всё лучшим приближением к 1/3.
Точно так же, сколько бы двоичных цифр вы ни захотели использовать, десятичное значение 0.1 не может быть точно представлено в виде двоичной дроби. В двоичной системе 1/10 – это бесконечно повторяющаяся дробь
0.0001100110011001100110011001100110011001100110011...
Остановитесь на любом конечном числе битов – и получите приближение. На большинстве современных машин числа с плавающей запятой приближаются с помощью двоичной дроби, в числителе используются первые 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.1000000000000000055511151231257827021181583404541015625 все аппроксимируются как 3602879701896397 / 2 ** 55. Поскольку все эти десятичные значения имеют одинаковое приближение, любое из них может быть отображено с сохранением инварианта eval(repr(x)) == x.
Исторически сложилось так, что приглашение Python и встроенная функция repr() выбирали число с 17 значащими цифрами, 0.10000000000000001. Начиная с Python 3.1, Python (на большинстве систем) теперь может выбирать кратчайшее из них и просто отображать 0.1.
Обратите внимание, что это в самой природе двоичной плавающей арифметики: это не ошибка в Python и не ошибка в вашем коде. То же самое вы увидите во всех языках, которые поддерживают арифметику с плавающей точкой вашего оборудования (хотя некоторые языки могут не показывать различие по умолчанию или во всех режимах вывода).
Для более приятного вывода можно использовать форматирование строк, чтобы получить ограниченное количество значащих цифр:
>>> format(math.pi, '.12g') # дать 12 значащих цифр
'3.14159265359'
>>> format(math.pi, '.2f') # дать 2 цифры после запятой
'3.14'
>>> repr(math.pi)
'3.141592653589793'
Важно понимать, что это, в определённом смысле, иллюзия: вы просто округляете отображение истинного машинного значения.
Одна иллюзия может породить другую. Например, поскольку 0.1 не равно в точности 1/10, суммирование трёх значений 0.1 может не дать в точности 0.3:
>>> .1 + .1 + .1 == .3
False
Кроме того, так как 0.1 не может быть ближе к точному значению 1/10, а 0.3 не может быть ближе к точному значению 3/10, то предварительное округление с помощью функции round() не помогает:
>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False
Хотя числа невозможно сделать ближе к их точным значениям, функция round() может быть полезна для последующего округления, чтобы результаты с неточными значениями стали сравнимы друг с другом:
>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True
Двоичная арифметика с плавающей запятой таит в себе множество подобных сюрпризов. Проблема с «0.1» подробно объясняется ниже, в разделе «Ошибка представления». См. Опасности чисел с плавающей запятой для более полного описания других распространённых сюрпризов.
Как сказано там ближе к концу, «лёгких ответов нет». Тем не менее не стоит чрезмерно опасаться плавающей точки! Ошибки в операциях с float в Python наследуются от оборудования для плавающей арифметики, и на большинстве машин они составляют не более 1 части на 2**53 на операцию. Этого более чем достаточно для большинства задач, но нужно помнить, что это не десятичная арифметика и что каждая операция с float может внести новую ошибку округления.
Хотя патологические случаи существуют, при обычном использовании арифметики с плавающей запятой в конечном итоге вы увидите ожидаемый результат, если просто округлить отображение окончательных результатов до нужного количества десятичных знаков. str() обычно достаточно, а для более точного управления смотрите str.format() спецификаторы формата метода в Синтаксис строк форматирования.
Для случаев, требующих точного десятичного представления, попробуйте использовать модуль decimal, который реализует десятичную арифметику, подходящую для бухгалтерских приложений и приложений с высокой точностью.
Другой вид точной арифметики поддерживается модулем fractions, который реализует арифметику на основе рациональных чисел (так что числа вроде 1/3 могут быть представлены точно).
Если вы активно используете операции с плавающей запятой, вам стоит взглянуть на пакет Numerical Python и многие другие пакеты для математических и статистических операций, предоставляемые проектом SciPy. См. <http://scipy.org>.
Python предоставляет инструменты, которые могут помочь в тех редких случаях, когда вы действительно хотите узнать точное значение числа с плавающей запятой. Метод float.as_integer_ratio() выражает значение числа с плавающей запятой в виде дроби:
>>> 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 и C99).
Ещё один полезный инструмент – функция math.fsum(), которая помогает смягчить потерю точности при суммировании. Она отслеживает «потерянные цифры» по мере добавления значений к накапливаемой сумме. Это может существенно повлиять на общую точность, чтобы ошибки не накапливались до такой степени, что влияют на конечный итог:
>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True
14.1. Ошибка представления¶Representation Error
В этом разделе подробно объясняется пример «0.1» и показывается, как можно самостоятельно выполнить точный анализ подобных случаев. Предполагается базовое знакомство с двоичным представлением чисел с плавающей запятой.
Ошибка представления относится к тому факту, что некоторые (на самом деле большинство) десятичные дроби не могут быть точно представлены в виде двоичных (с основанием 2) дробей. Это главная причина, по которой Python (или Perl, C, C++, Java, Fortran и многие другие) часто не отображает точное десятичное число, как ожидается.
Почему так? 1/10 не может быть точно представлено в виде двоичной дроби. Почти все современные машины (ноябрь 2000 года) используют арифметику с плавающей запятой IEEE-754, и почти все платформы отображают числа с плавающей запятой Python на «двойную точность» IEEE-754. Числа двойной точности 754 содержат 53 бита точности, поэтому при вводе компьютер стремится преобразовать 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
что означает, что точное число, хранящееся в компьютере, равно десятичному значению 0.1000000000000000055511151231257827021181583404541015625. Вместо отображения полного десятичного значения многие языки (включая старые версии Python) округляют результат до 17 значащих цифр:
>>> format(0.1, '.17f')
'0.10000000000000001'
Модули fractions и 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'