Содержание страницы
15. Арифметика с плавающей точкой: проблемы и ограничения¶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, могут быть представлены точно).
Если вы интенсивно используете операции с плавающей запятой, стоит взглянуть на пакет NumPy и многие другие пакеты для математических и статистических операций, предоставляемые проектом SciPy. Смотрите <https://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
15.1. Ошибка представления¶Representation Error
В этом разделе подробно объясняется пример «0.1» и показывается, как можно самостоятельно выполнить точный анализ подобных случаев. Предполагается базовое знакомство с двоичным представлением чисел с плавающей запятой.
Ошибка представления относится к тому факту, что некоторые (на самом деле большинство) десятичные дроби не могут быть точно представлены в виде двоичных (с основанием 2) дробей. Это главная причина, по которой Python (или Perl, C, C++, Java, Fortran и многие другие) часто не отображает точное десятичное число, как ожидается.
Почему так? 1/10 не может быть точно представлено в виде двоичной дроби. Начиная как минимум с 2000 года, почти все машины используют двоичную арифметику с плавающей запятой IEEE 754, и почти все платформы отображают числа с плавающей запятой Python на двоичные значения двойной точности IEEE 754 binary64. Значения binary64 содержат 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 в двойной точности IEEE 754:
7205759403792794 / 2 ** 56
Деление числителя и знаменателя на два даёт дробь:
3602879701896397 / 2 ** 55
Обратите внимание, что, поскольку мы округлили вверх, это значение на самом деле немного больше 1/10; если бы мы не округлили вверх, частное было бы немного меньше 1/10. Но в любом случае оно не может быть точно 1/10!
Итак, компьютер никогда не «видит» 1/10: он видит точную дробь, указанную выше, – наилучшее приближение двойной точности IEEE 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'