> **Источник:** https://python-all.ru/3.2/tutorial/floatingpoint.html
>
> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.

---

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

Числа с плавающей запятой представляются в компьютерном оборудовании в виде двоичных дробей (по основанию 2). Например, десятичная дробь

```python
0.125
```

имеет значение 1/10 + 2/100 + 5/1000, и точно так же двоичная дробь

```python
0.001
```

имеет значение 0/2 + 0/4 + 1/8. Эти две дроби имеют одинаковые значения, единственное различие в том, что первая записана в десятичной системе счисления, а вторая – в двоичной.

К сожалению, большинство десятичных дробей не могут быть точно представлены в виде двоичных дробей. Следствием этого является то, что, вообще говоря, вводимые десятичные числа с плавающей запятой лишь приблизительно соответствуют двоичным числам с плавающей запятой, которые на самом деле хранятся в машине.

Проблему легче сначала понять в десятичной системе. Рассмотрим дробь 1/3. Её можно приблизить десятичной дробью:

```python
0.3
```

или, лучше,

```python
0.33
```

или, лучше,

```python
0.333
```

и так далее. Сколько бы цифр вы ни захотели записать, результат никогда не будет точно равен 1/3, но будет всё лучшим приближением к 1/3.

Точно так же, сколько бы двоичных цифр вы ни захотели использовать, десятичное значение 0.1 не может быть точно представлено в виде двоичной дроби. В двоичной системе 1/10 – это бесконечно повторяющаяся дробь

```python
0.0001100110011001100110011001100110011001100110011...
```

Остановитесь на любом конечном числе битов – и получите приближение. На большинстве современных машин числа с плавающей запятой приближаются с помощью двоичной дроби, в числителе используются первые 53 бита, начиная со старшего значащего бита, а знаменатель – степень двойки. Для 1/10 двоичная дробь равна `3602879701896397 / 2 ** 55`, что близко, но не в точности равно истинному значению 1/10.

Многие пользователи не подозревают о приближении из-за того, как отображаются значения. Python печатает лишь десятичное приближение к истинному десятичному значению того двоичного приближения, которое хранит машина. На большинстве машин, если бы Python печатал истинное десятичное значение двоичного приближения, хранящегося для 0.1, ему пришлось бы отобразить

```python
>>> 0.1
0.1000000000000000055511151231257827021181583404541015625
```

Это больше цифр, чем обычно нужно, поэтому Python сохраняет число цифр управляемым, отображая вместо этого округлённое значение

```python
>>> 1 / 10
0.1
```

Просто помните: хотя напечатанный результат выглядит как точное значение 1/10, на самом деле хранится ближайшая представимая двоичная дробь.

Интересно, что существует много разных десятичных чисел, которым соответствует одна и та же ближайшая аппроксимирующая двоичная дробь. Например, числа `0.1` и `0.10000000000000001` и `0.1000000000000000055511151231257827021181583404541015625` все аппроксимируются как `3602879701896397 / 2 ** 55`. Поскольку все эти десятичные значения имеют одинаковое приближение, любое из них может быть отображено с сохранением инварианта `eval(repr(x)) == x`.

Исторически сложилось так, что приглашение Python и встроенная функция [`repr()`](https://python-all.ru/3.2/library/functions.html#repr) выбирали число с 17 значащими цифрами, `0.10000000000000001`. Начиная с Python 3.1, Python (на большинстве систем) теперь может выбирать кратчайшее из них и просто отображать `0.1`.

Обратите внимание, что это в самой природе двоичной плавающей арифметики: это не ошибка в Python и не ошибка в вашем коде. То же самое вы увидите во всех языках, которые поддерживают арифметику с плавающей точкой вашего оборудования (хотя некоторые языки могут не *показывать* различие по умолчанию или во всех режимах вывода).

Для более приятного вывода можно использовать форматирование строк, чтобы получить ограниченное количество значащих цифр:

```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:

```python
>>> .1 + .1 + .1 == .3
False
```

Кроме того, так как 0.1 не может быть ближе к точному значению 1/10, а 0.3 не может быть ближе к точному значению 3/10, то предварительное округление с помощью функции [`round()`](https://python-all.ru/3.2/library/functions.html#round) не помогает:

```python
>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False
```

Хотя числа невозможно сделать ближе к их точным значениям, функция [`round()`](https://python-all.ru/3.2/library/functions.html#round) может быть полезна для последующего округления, чтобы результаты с неточными значениями стали сравнимы друг с другом:

```python
>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True
```

Двоичная арифметика с плавающей запятой таит в себе множество подобных сюрпризов. Проблема с «0.1» подробно объясняется ниже, в разделе «Ошибка представления». См. [Опасности чисел с плавающей запятой](https://python-all.ru/3.2/tutorial/floatingpoint.html) для более полного описания других распространённых сюрпризов.

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

Хотя патологические случаи существуют, при обычном использовании арифметики с плавающей запятой в конечном итоге вы увидите ожидаемый результат, если просто округлить отображение окончательных результатов до нужного количества десятичных знаков. [`str()`](https://python-all.ru/3.2/library/functions.html#str) обычно достаточно, а для более точного управления смотрите [`str.format()`](https://python-all.ru/3.2/library/stdtypes.html#str.format) спецификаторы формата метода в [*Синтаксис строк форматирования*](https://python-all.ru/3.2/library/string.html#formatstrings).

Для случаев, требующих точного десятичного представления, попробуйте использовать модуль [`decimal`](https://python-all.ru/3.2/library/decimal.html#module-decimal), который реализует десятичную арифметику, подходящую для бухгалтерских приложений и приложений с высокой точностью.

Другой вид точной арифметики поддерживается модулем [`fractions`](https://python-all.ru/3.2/library/fractions.html#module-fractions), который реализует арифметику на основе рациональных чисел (так что числа вроде 1/3 могут быть представлены точно).

Если вы активно используете операции с плавающей запятой, вам стоит взглянуть на пакет Numerical Python и многие другие пакеты для математических и статистических операций, предоставляемые проектом SciPy. См. \<[http://scipy.org](https://python-all.ru/3.2/tutorial/floatingpoint.html)\>.

Python предоставляет инструменты, которые могут помочь в тех редких случаях, когда вы действительно *хотите* узнать точное значение числа с плавающей запятой. Метод [`float.as_integer_ratio()`](https://python-all.ru/3.2/library/stdtypes.html#float.as_integer_ratio) выражает значение числа с плавающей запятой в виде дроби:

```python
>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)
```

Поскольку отношение точное, его можно использовать для восстановления исходного значения без потерь:

```python
>>> x == 3537115888337719 / 1125899906842624
True
```

Метод [`float.hex()`](https://python-all.ru/3.2/library/stdtypes.html#float.hex) выражает число с плавающей запятой в шестнадцатеричном формате (основание 16), снова предоставляя точное значение, хранящееся на вашем компьютере:

```python
>>> x.hex()
'0x1.921f9f01b866ep+1'
```

Это точное шестнадцатеричное представление можно использовать для точного восстановления значения числа с плавающей запятой:

```python
>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True
```

Поскольку представление точное, оно полезно для надёжного переноса значений между разными версиями Python (независимость от платформы) и обмена данными с другими языками, поддерживающими тот же формат (например, Java и C99).

Ещё один полезный инструмент – функция [`math.fsum()`](https://python-all.ru/3.2/library/math.html#math.fsum), которая помогает смягчить потерю точности при суммировании. Она отслеживает «потерянные цифры» по мере добавления значений к накапливаемой сумме. Это может существенно повлиять на общую точность, чтобы ошибки не накапливались до такой степени, что влияют на конечный итог:

```python
>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True
```

## 14.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 бита. Переписывая

```python
1 / 10 ~= J / (2**N)
```

как

```python
J ~= 2**N / 10
```

и вспоминая, что *J* имеет ровно 53 бита (`>= 2**52`, но `< 2**53`), наилучшим значением для *N* является 56:

```python
>>> 2**52 <=  2**56 // 10  < 2**53
True
```

То есть 56 – единственное значение для *N*, которое оставляет *J* с ровно 53 битами. Наилучшее возможное значение для *J* тогда – это округлённое частное:

```python
>>> q, r = divmod(2**56, 10)
>>> r
6
```

Поскольку остаток больше половины 10, наилучшее приближение получается округлением вверх:

```python
>>> q+1
7205759403792794
```

Следовательно, наилучшее возможное приближение к 1/10 в двойной точности 754 – это:

```python
7205759403792794 / 2 ** 56
```

Деление числителя и знаменателя на два даёт дробь:

```python
3602879701896397 / 2 ** 55
```

Обратите внимание, что, поскольку мы округлили вверх, это значение на самом деле немного больше 1/10; если бы мы не округлили вверх, частное было бы немного меньше 1/10. Но в любом случае оно не может быть *точно* 1/10!

Таким образом, компьютер никогда не «видит» 1/10: он видит точную дробь, приведённую выше, – наилучшее приближение двойной точности 754, которое он может получить:

```python
>>> 0.1 * 2 ** 55
3602879701896397.0
```

Если умножить эту дробь на 10\*\*55, мы увидим значение с точностью до 55 десятичных знаков:

```python
>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625
```

что означает, что точное число, хранящееся в компьютере, равно десятичному значению 0.1000000000000000055511151231257827021181583404541015625. Вместо отображения полного десятичного значения многие языки (включая старые версии Python) округляют результат до 17 значащих цифр:

```python
>>> format(0.1, '.17f')
'0.10000000000000001'
```

Модули [`fractions`](https://python-all.ru/3.2/library/fractions.html#module-fractions) и [`decimal`](https://python-all.ru/3.2/library/decimal.html#module-decimal) упрощают эти вычисления:

```python
>>> 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'
```
