Spec-Zone.ru › Python 3.11

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

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

Как сказано в конце, «нет простых ответов». Тем не менее, не будьте излишне настороженными в отношении чисел с плавающей точкой! Ошибки в операциях с плавающей точкой в 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 float в значения IEEE 754 binary64 «с двойной точностью». Значения IEEE 754 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 double, которое он может получить:

>>> 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–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/tutorial/floatingpoint.html

Spec-Zone.ru

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