Spec-Zone.ru › Python 3.9

Арифметика с плавающей запятой: проблемы и ограничения

Числа с плавающей запятой представляются в компьютерном оборудовании в виде двоичных дробей (основание 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')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'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» подробно объяснена ниже в разделе «Ошибка представления». См. The Perils of Floating Point для более подробного описания других распространённых сюрпризов.

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

Хотя патологические случаи существуют, для большинства случаев использования арифметики с плавающей запятой вы увидите ожидаемый результат в итоге, если просто округлите отображаемые конечные результаты до требуемого числа десятичных цифр. 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. Ошибка представления

В этом разделе подробно объясняется пример «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'

© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/tutorial/floatingpoint.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API