Документация Python неофициальный перевод

floatingpoint.md

270 строк · 21.8 КБ · обычная страница · сырой текст · скачать

1> **Источник:** https://python-all.ru/3.16/tutorial/floatingpoint.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# 15. Арифметика с плавающей запятой: проблемы и ограничения89Числа с плавающей запятой представляются в компьютерном оборудовании в виде дробей по основанию 2 (двоичных). Например, **десятичная** дробь `0.625` имеет значение 6/10 + 2/100 + 5/1000, и точно так же **двоичная** дробь `0.101` имеет значение 1/2 + 0/4 + 1/8. Эти две дроби имеют одинаковые значения, единственное реальное различие в том, что первая записана в десятичной дробной записи, а вторая – в двоичной.1011К сожалению, большинство десятичных дробей не могут быть точно представлены в виде двоичных дробей. Следствием этого является то, что, вообще говоря, вводимые десятичные числа с плавающей запятой лишь приблизительно соответствуют двоичным числам с плавающей запятой, которые на самом деле хранятся в машине.1213Проблему легче сначала понять в десятичной системе. Рассмотрим дробь 1/3. Её можно приблизить десятичной дробью:1415```python160.317```1819или, лучше,2021```python220.3323```2425или, лучше,2627```python280.33329```3031и так далее. Сколько бы цифр вы ни захотели записать, результат никогда не будет точно равен 1/3, но будет всё лучшим приближением к 1/3.3233Точно так же, сколько бы двоичных цифр вы ни захотели использовать, десятичное значение 0.1 не может быть точно представлено в виде двоичной дроби. В двоичной системе 1/10 – это бесконечно повторяющаяся дробь3435```python360.0001100110011001100110011001100110011001100110011...37```3839Остановитесь на любом конечном числе битов – и получите приближение. На большинстве современных машин числа с плавающей запятой приближаются с помощью двоичной дроби, в числителе используются первые 53 бита, начиная со старшего значащего бита, а знаменатель – степень двойки. Для 1/10 двоичная дробь равна `3602879701896397 / 2 ** 55`, что близко, но не в точности равно истинному значению 1/10.4041Многие пользователи не замечают этого приближения из-за того, как отображаются значения. Python печатает лишь десятичное приближение к истинному десятичному значению того двоичного приближения, которое хранится в машине. На большинстве машин, если бы Python выводил истинное десятичное значение двоичного приближения, хранящегося для 0.1, ему пришлось бы показать:4243```python44>>> 0.1450.100000000000000005551115123125782702118158340454101562546```4748Это больше цифр, чем нужно большинству людей, поэтому Python сохраняет количество цифр в разумных пределах, отображая вместо этого округлённое значение:4950```pycon51>>> 1 / 10520.153```5455Просто помните: хотя напечатанный результат выглядит как точное значение 1/10, на самом деле хранится ближайшая представимая двоичная дробь.5657Интересно, что существует множество различных десятичных чисел, имеющих одно и то же ближайшее приближение в виде двоичной дроби. Например, числа `0.1`, `0.10000000000000001` и `0.1000000000000000055511151231257827021181583404541015625` все приближаются `3602879701896397 / 2 ** 55`. Поскольку все эти десятичные значения имеют одинаковое приближение, любое из них может быть отображено с сохранением инварианта `eval(repr(x)) == x`.5859Исторически сложилось так, что приглашение Python и встроенная функция [`repr()`](https://python-all.ru/3.16/library/functions.html#repr) выбирали значение с 17 значащими цифрами, `0.10000000000000001`. Начиная с Python 3.1, Python (на большинстве систем) теперь способен выбирать самое короткое из них и просто отображать `0.1`.6061Обратите внимание: это сама суть двоичной арифметики с плавающей запятой – это не ошибка в Python и не ошибка в вашем коде. Вы увидите то же самое во всех языках, поддерживающих аппаратную арифметику с плавающей запятой (хотя некоторые языки могут не *отображать* разницу по умолчанию или во всех режимах вывода).6263Для более удобного вывода можно использовать форматирование строк, чтобы получить ограниченное количество значащих цифр:6465```pycon66>>> format(math.pi, '.12g')  # дать 12 значащих цифр67'3.14159265359'6869>>> format(math.pi, '.2f')   # дать 2 цифры после запятой70'3.14'7172>>> repr(math.pi)73'3.141592653589793'74```7576Важно понимать, что это, в определённом смысле, иллюзия: вы просто округляете *отображение* истинного машинного значения.7778Одна иллюзия может породить другую. Например, поскольку 0.1 не равно в точности 1/10, суммирование трёх значений 0.1 может не дать в точности 0.3:7980```pycon81>>> 0.1 + 0.1 + 0.1 == 0.382False83```8485Кроме того, поскольку 0.1 не может быть ближе к точному значению 1/10, а 0.3 не может быть ближе к точному значению 3/10, то предварительное округление с помощью функции [`round()`](https://python-all.ru/3.16/library/functions.html#round) не поможет:8687```pycon88>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)89False90```9192Хотя числа нельзя сделать ближе к их точным значениям, функция [`math.isclose()`](https://python-all.ru/3.16/library/math.html#math.isclose) может быть полезна для сравнения неточных значений:9394```pycon95>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)96True97```9899В качестве альтернативы функция [`round()`](https://python-all.ru/3.16/library/functions.html#round) может использоваться для сравнения грубых приближений:100101```pycon102>>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)103True104```105106Двоичная арифметика с плавающей запятой таит много подобных сюрпризов. Проблема с «0.1» подробно объясняется ниже, в разделе «Ошибка представления». Смотрите [Примеры проблем с плавающей запятой](https://python-all.ru/3.16/tutorial/floatingpoint.html) для хорошего обзора того, как работает двоичная арифметика с плавающей запятой и какие проблемы обычно встречаются на практике. Также смотрите [Опасности плавающей запятой](https://python-all.ru/3.16/tutorial/floatingpoint.html) для более полного описания других распространённых сюрпризов.107108Как сказано ближе к концу, «лёгких ответов нет». Тем не менее, не стоит чрезмерно опасаться плавающей запятой! Ошибки в операциях Python с числами с плавающей запятой наследуются от аппаратного обеспечения и на большинстве машин составляют не более 1 части на 2\*\*53 на операцию. Этого более чем достаточно для большинства задач, но нужно помнить, что это не десятичная арифметика и что каждая операция с плавающей запятой может дать новую ошибку округления.109110Хотя патологические случаи существуют, при обычном использовании арифметики с плавающей запятой вы в конечном итоге увидите ожидаемый результат, если просто округлите отображение конечных результатов до ожидаемого количества десятичных знаков. Обычно достаточно [`str()`](https://python-all.ru/3.16/library/stdtypes.html#str), а для более точного контроля смотрите спецификаторы формата метода [`str.format()`](https://python-all.ru/3.16/library/stdtypes.html#str.format) в [Синтаксисе строк форматирования](https://python-all.ru/3.16/library/string.html#formatstrings).111112Для случаев, требующих точного десятичного представления, попробуйте использовать модуль [`decimal`](https://python-all.ru/3.16/library/decimal.html#module-decimal), который реализует десятичную арифметику, подходящую для бухгалтерских приложений и приложений с высокой точностью.113114Другая форма точной арифметики поддерживается модулем [`fractions`](https://python-all.ru/3.16/library/fractions.html#module-fractions), который реализует арифметику на основе рациональных чисел (так что такие числа, как 1/3, могут быть представлены точно).115116Если вы интенсивно используете операции с плавающей запятой, стоит взглянуть на пакет NumPy и многие другие пакеты для математических и статистических операций, предоставляемые проектом SciPy. Смотрите \<[https://scipy.org](https://python-all.ru/3.16/tutorial/floatingpoint.html)\>.117118Python предоставляет инструменты, которые могут помочь в тех редких случаях, когда *действительно* нужно узнать точное значение числа с плавающей запятой. Метод [`float.as_integer_ratio()`](https://python-all.ru/3.16/library/stdtypes.html#float.as_integer_ratio) представляет значение числа с плавающей запятой в виде дроби:119120```pycon121>>> x = 3.14159122>>> x.as_integer_ratio()123(3537115888337719, 1125899906842624)124```125126Поскольку отношение точное, его можно использовать для восстановления исходного значения без потерь:127128```pycon129>>> x == 3537115888337719 / 1125899906842624130True131```132133Метод [`float.hex()`](https://python-all.ru/3.16/library/stdtypes.html#float.hex) представляет число с плавающей запятой в шестнадцатеричном виде (с основанием 16), снова выдавая точное значение, хранящееся в компьютере:134135```pycon136>>> x.hex()137'0x1.921f9f01b866ep+1'138```139140Это точное шестнадцатеричное представление можно использовать для точного восстановления значения числа с плавающей запятой:141142```pycon143>>> x == float.fromhex('0x1.921f9f01b866ep+1')144True145```146147Поскольку представление точное, оно полезно для надёжного переноса значений между разными версиями Python (независимость от платформы) и обмена данными с другими языками, поддерживающими тот же формат (например, Java и C99).148149Ещё одним полезным инструментом является функция [`sum()`](https://python-all.ru/3.16/library/functions.html#sum), которая помогает уменьшить потерю точности при суммировании. Она использует расширенную точность для промежуточных округлений по мере добавления значений к накапливаемой сумме. Это может повлиять на общую точность, так что ошибки не накапливаются до такой степени, чтобы повлиять на конечный результат:150151```pycon152>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0153False154>>> sum([0.1] * 10) == 1.0155True156```157158Функция [`math.fsum()`](https://python-all.ru/3.16/library/math.html#math.fsum) идёт дальше и отслеживает все «потерянные цифры» по мере добавления значений к накапливаемой сумме, так что результат округляется только один раз. Это медленнее, чем [`sum()`](https://python-all.ru/3.16/library/functions.html#sum), но будет точнее в редких случаях, когда входные значения большой величины в основном взаимно уничтожаются, оставляя итоговую сумму, близкую к нулю:159160```pycon161>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,162...        -143401161400469.7, 266262841.31058735, -0.003244936839808227]163>>> float(sum(map(Fraction, arr)))   # Точное суммирование с однократным округлением1648.042173697819788e-13165>>> math.fsum(arr)                   # Однократное округление1668.042173697819788e-13167>>> sum(arr)                         # Многократное округление с расширенной точностью1688.042178034628478e-13169>>> total = 0.0170>>> for x in arr:171...     total += x                   # Многократное округление со стандартной точностью172...173>>> total                            # Прямое сложение не даёт ни одной правильной цифры!174-0.0051575902860057365175```176177## 15.1. Ошибка представления178179В этом разделе подробно объясняется пример «0.1» и показывается, как можно самостоятельно выполнить точный анализ подобных случаев. Предполагается базовое знакомство с двоичным представлением чисел с плавающей запятой.180181*Ошибка представления* относится к тому факту, что некоторые (на самом деле большинство) десятичные дроби не могут быть точно представлены в виде двоичных (с основанием 2) дробей. Это главная причина, по которой Python (или Perl, C, C++, Java, Fortran и многие другие) часто не отображает точное десятичное число, как ожидается.182183Почему так? 1/10 не может быть точно представлено в виде двоичной дроби. Начиная как минимум с 2000 года, почти все машины используют двоичную арифметику с плавающей запятой IEEE 754, и почти все платформы отображают числа с плавающей запятой Python на двоичные значения двойной точности IEEE 754 binary64. Значения binary64 содержат 53 бита точности, поэтому при вводе компьютер стремится преобразовать 0.1 в ближайшую возможную дробь вида *J*/2\*\**N*, где *J* – целое число, содержащее ровно 53 бита. Переписывая184185```python1861 / 10 ~= J / (2**N)187```188189как190191```python192J ~= 2**N / 10193```194195и вспоминая, что *J* имеет ровно 53 бита (`>= 2**52`, но `< 2**53`), наилучшим значением для *N* является 56:196197```pycon198>>> 2**52 <=  2**56 // 10  < 2**53199True200```201202То есть 56 – единственное значение для *N*, которое оставляет *J* с ровно 53 битами. Наилучшее возможное значение для *J* тогда – это округлённое частное:203204```pycon205>>> q, r = divmod(2**56, 10)206>>> r2076208```209210Поскольку остаток больше половины 10, наилучшее приближение получается округлением вверх:211212```pycon213>>> q+12147205759403792794215```216217Следовательно, наилучшее возможное приближение к 1/10 в двойной точности IEEE 754:218219```python2207205759403792794 / 2 ** 56221```222223Деление числителя и знаменателя на два даёт дробь:224225```python2263602879701896397 / 2 ** 55227```228229Обратите внимание, что, поскольку мы округлили вверх, это значение на самом деле немного больше 1/10; если бы мы не округлили вверх, частное было бы немного меньше 1/10. Но в любом случае оно не может быть *точно* 1/10!230231Итак, компьютер никогда не «видит» 1/10: он видит точную дробь, указанную выше, – наилучшее приближение двойной точности IEEE 754, которое он может получить:232233```pycon234>>> 0.1 * 2 ** 552353602879701896397.0236```237238Если умножить эту дробь на 10\*\*55, мы увидим значение с точностью до 55 десятичных знаков:239240```pycon241>>> 3602879701896397 * 10 ** 55 // 2 ** 552421000000000000000055511151231257827021181583404541015625243```244245что означает, что точное число, хранящееся в компьютере, равно десятичному значению 0.1000000000000000055511151231257827021181583404541015625. Вместо отображения полного десятичного значения многие языки (включая старые версии Python) округляют результат до 17 значащих цифр:246247```pycon248>>> format(0.1, '.17f')249'0.10000000000000001'250```251252Модули [`fractions`](https://python-all.ru/3.16/library/fractions.html#module-fractions) и [`decimal`](https://python-all.ru/3.16/library/decimal.html#module-decimal) упрощают эти вычисления:253254```pycon255>>> from decimal import Decimal256>>> from fractions import Fraction257258>>> Fraction.from_float(0.1)259Fraction(3602879701896397, 36028797018963968)260261>>> (0.1).as_integer_ratio()262(3602879701896397, 36028797018963968)263264>>> Decimal.from_float(0.1)265Decimal('0.1000000000000000055511151231257827021181583404541015625')266267>>> format(Decimal.from_float(0.1), '.17')268'0.10000000000000001'269```270