Вещественные числа float в Python: точность и погрешность
Обновлено:
Вещественные числа мы уже использовали:
temperature = 18.5
price = 1250.75
distance = 3.14
Все эти объекты относятся к классу:
float
На этом уроке мы рассмотрим важную особенность float: многие десятичные дроби хранятся в компьютере не абсолютно точно, а в виде ближайшего возможного значения.
1. Неожиданный результат
Начнём с обычного сравнения:
print(0.3 == 0.3)
Результат:
True
Теперь выполним сложение:
print(0.1 + 0.2 == 0.3)
Результат:
False
Математически:
0.1 + 0.2 = 0.3
Но компьютер получил немного отличающиеся значения.
Посмотрим на результат сложения:
print(0.1 + 0.2)
Результат:
0.30000000000000004
Полученное число очень близко к 0.3, но не равно ему полностью.
2. Это не ошибка Python
Неточность появляется не из-за ошибки в Python и не из-за неправильно написанного кода.
Большинство современных компьютеров хранит вещественные числа в двоичном формате IEEE 754. Многие десятичные дроби невозможно точно записать с помощью конечного количества двоичных цифр. Поэтому компьютер сохраняет ближайшее доступное значение.
Подобное поведение встречается во многих языках:
- Python;
- C;
- C++;
- Java;
- JavaScript;
- C#;
- Fortran.
Например, тип Number в JavaScript тоже основан на 64-битном формате двойной точности IEEE 754, а тип double в Java соответствует тому же формату.
3. Десятичные и двоичные дроби
Люди обычно используют десятичную систему счисления:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Компьютер хранит данные в двоичной системе:
0 и 1
Некоторые десятичные дроби можно точно представить в двоичной системе.
Например:
0.5 = 1 / 2
0.25 = 1 / 4
0.125 = 1 / 8
Поэтому такие вычисления могут выполняться точно:
print(0.5 + 0.25 == 0.75)
Результат:
True
Но число 0.1 не имеет конечной двоичной записи.
Его двоичное представление продолжается бесконечно, примерно как число 1 / 3 в десятичной системе:
1 / 3 = 0.333333333333...
Компьютер не может хранить бесконечное количество цифр, поэтому вынужден остановиться и округлить значение.
4. Как на самом деле хранится 0.1
Когда мы пишем:
number = 0.1
Python показывает:
0.1
Но внутри хранится ближайшая двоичная дробь.
Её точное десятичное значение выглядит так:
0.1000000000000000055511151231257827021181583404541015625
Python специально выводит короткую и удобную запись 0.1, скрывая незначительную погрешность.
5. Просмотр точного значения через Decimal
Для демонстрации можно использовать класс Decimal:
from decimal import Decimal
print(Decimal.from_float(0.1))
Результат:
0.1000000000000000055511151231257827021181583404541015625
Для 0.2:
print(Decimal.from_float(0.2))
Результат:
0.200000000000000011102230246251565404236316680908203125
Для обычного 0.3:
print(Decimal.from_float(0.3))
Результат:
0.299999999999999988897769753748434595763683319091796875
А результат 0.1 + 0.2 хранится так:
print(Decimal.from_float(0.1 + 0.2))
Результат:
0.3000000000000000444089209850062616169452667236328125
Поэтому сравнение возвращает False:
0.1 + 0.2 == 0.3
Слева и справа находятся близкие, но немного разные значения.
Decimal.from_float() используется здесь только для демонстрации точного значения уже созданного объекта float.
6. Создание точного Decimal
Важно различать два варианта.
Создание из объекта float:
Decimal.from_float(0.1)
показывает погрешность, которая уже появилась при создании float.
Создание из строки:
Decimal('0.1')
создаёт точное десятичное значение:
0.1
Поэтому для точных денежных расчётов значения Decimal обычно создают из строк, а не из объектов float:
from decimal import Decimal
price = Decimal('1250.75')
Сам класс Decimal будет подробно изучаться позже.
Устройство float
7. Двойная точность
На большинстве современных платформ Python использует для float формат IEEE 754 binary64, который также называется:
double precision
или:
двойная точность
Для представления числа используется 64 бита:
1 бит — знак
11 бит — экспонента
52 бита — дробная часть значащего числа
При этом для обычных нормализованных чисел используется ещё одна подразумеваемая начальная единица. Поэтому фактическая точность составляет 53 двоичных разряда, хотя физически для дробной части записываются 52 бита.
8. За что отвечают части числа
Упрощённо устройство можно представить так:
знак | экспонента | значащая часть
Знак
Определяет, является число положительным или отрицательным.
Экспонента
Определяет порядок числа — насколько далеко должна переместиться двоичная точка.
Благодаря экспоненте float умеет представлять как очень большие, так и очень маленькие значения.
Значащая часть
Хранит основные значащие двоичные цифры числа.
Именно ограниченный размер этой части приводит к округлениям.
9. 52 бита — не максимальный размер числа
Неправильно считать, что число из 52 единиц является максимальным значением, которое может хранить float.
52 бита ограничивают количество хранимых цифр значащей части, но не общий размер значения.
Положение точки определяется экспонентой, поэтому float может хранить числа примерно до:
1.7976931348623157e+308
Получить характеристики float можно через sys.float_info:
import sys
print(sys.float_info.max)
print(sys.float_info.mant_dig)
print(sys.float_info.dig)
Типичный результат:
1.7976931348623157e+308
53
15
mant_dig показывает 53 двоичных разряда точности, а dig — гарантированное количество значащих десятичных цифр для безопасного преобразования туда и обратно.
10. Значащие цифры
Обычно говорят, что binary64 обеспечивает примерно:
15–17 значащих десятичных цифр
Речь идёт не обязательно о цифрах после точки, а обо всех значащих цифрах числа.
Например, в числе:
1234567890123456.0
учитываются цифры как до точки, так и после неё.
Нельзя сформулировать правило так:
Python всегда удаляет семнадцатую цифру.
Результат зависит от:
- значения числа;
- его порядка;
- возможности представить его в двоичной системе;
- ближайших доступных значений
float.
11. Точность больших целых значений внутри float
Рассмотрим два соседних целых числа:
first_number = float(2 ** 53)
second_number = float(2 ** 53 + 1)
print(first_number)
print(second_number)
print(first_number == second_number)
Результат:
9007199254740992.0
9007199254740992.0
True
Число:
2 ** 53 + 1
уже не может быть представлено отдельным значением в формате binary64 и округляется до ближайшего доступного числа.
Это не означает, что float не может быть больше 2 ** 53.
Он может хранить значительно большие числа, но между соседними доступными значениями начинают появляться промежутки.
12. Экспоненциальная нотация не создаёт погрешность
Большие числа могут выводиться так:
number = 1.5e20
print(number)
Результат:
1.5e+20
Экспоненциальная нотация — это только удобный способ показать значение.
Сам переход к такой записи не является причиной потери точности.
Погрешность возникает из-за ограниченного набора двоичных значений, которые способен хранить float.
13. Слишком большое значение
Если результат превышает максимальное конечное значение float, можно получить бесконечность:
number = 1e308
print(number)
Результат:
1e+308
Но преобразование ещё большего значения:
number = float('1e1000')
print(number)
даст:
inf
inf означает положительную бесконечность:
infinity
Размер объектов в памяти
14. Числовое значение и объект Python
Само значение binary64 занимает:
64 бита = 8 байт
Но объект float в CPython занимает больше памяти.
Причина в том, что каждый объект Python дополнительно содержит служебную информацию. В обычной сборке CPython в заголовке объекта находятся, в частности:
- счётчик ссылок;
- ссылка на класс объекта.
После заголовка хранится само числовое значение double. Методы не копируются внутрь каждого числа: они принадлежат классу float.
15. Проверка размера
Для проверки размера объекта удобно использовать:
from sys import getsizeof
Пример:
from sys import getsizeof
print(getsizeof(1))
print(getsizeof(1.0))
На 64-битном CPython 3.13 можно получить:
28
24
То есть в такой среде:
объект int 1 → 28 байт
объект float 1.0 → 24 байта
Результаты являются особенностью конкретной реализации, версии и платформы. В другой среде значения могут отличаться.
16. Метод __sizeof__()
Размер можно посмотреть и через специальный метод:
print((1).__sizeof__())
print((1.0).__sizeof__())
Однако в обычном коде для измерения размера предпочтительнее использовать:
sys.getsizeof()
Она явно показывает назначение операции и дополнительно учитывает служебный размер сборщика мусора для тех объектов, к которым это применимо.
17. Размер int зависит от значения
Целые числа Python имеют произвольную точность.
По мере увеличения числа объект int занимает больше памяти:
from sys import getsizeof
small_number = 1
large_number = 10 ** 50
very_large_number = 10 ** 300
print(getsizeof(small_number))
print(getsizeof(large_number))
print(getsizeof(very_large_number))
На 64-битном CPython 3.13 можно получить:
28
48
160
Конкретные размеры зависят от среды, но общий принцип остаётся одинаковым:
Чем больше значащих двоичных цифр требуется для
int, тем больше памяти занимает объект.
18. Размер float не растёт вместе с числом
from sys import getsizeof
small_number = 1.0
large_number = 1e100
very_large_number = 1e300
print(getsizeof(small_number))
print(getsizeof(large_number))
print(getsizeof(very_large_number))
В типичной 64-битной версии CPython:
24
24
24
Все объекты float используют один и тот же фиксированный формат.
Большое значение не заставляет объект увеличиваться. Вместо этого снижается расстояние точности между соседними представимыми числами.
19. Преимущество и недостаток float
Главный недостаток:
ограниченная точность
Главные преимущества:
- фиксированный размер;
- огромный диапазон значений;
- аппаратная поддержка процессором;
- высокая скорость вычислений;
- возможность хранить дробные значения.
Поэтому float отлично подходит для задач, в которых небольшая погрешность допустима.
Где используется float
20. Погодные данные
Предположим, сервис хранит данные от большого количества метеостанций:
- температуру воздуха;
- ощущаемую температуру;
- скорость ветра;
- направление ветра;
- высоту волн;
- количество осадков;
- толщину снежного покрова;
- атмосферное давление.
Для пользователя обычно достаточно увидеть:
Температура: 18.4 °C
Скорость ветра: 3.7 м/с
Высота волны: 1.2 м
Погрешность на уровне пятнадцатого знака не имеет практического значения.
При этом значений может быть очень много, и над ними необходимо быстро выполнять расчёты. Для такой задачи float подходит хорошо.
21. Координаты и маршруты
В географических и спортивных приложениях могут храниться:
latitude = 43.0410
longitude = 42.7297
distance = 27.35
elevation = 1060.5
speed = 18.7
Небольшая машинная погрешность обычно менее значительна, чем погрешность самого GPS-датчика.
float позволяет быстро обрабатывать координаты, расстояния, скорости и набор высоты.
22. Аналитика и машинное обучение
В аналитических системах часто рассчитываются:
- средние значения;
- проценты;
- коэффициенты;
- вероятности;
- статистические показатели;
- результаты моделей.
conversion_rate = 0.0345
average_order_value = 2540.75
model_probability = 0.8721
Во многих таких задачах абсолютная точность каждой последней цифры не требуется, а скорость обработки больших объёмов данных важна.
23. Графика, физика и игры
float используется для представления:
- координат объектов;
- скорости;
- ускорения;
- направления движения;
- размеров;
- углов;
- времени между кадрами.
object_x = 125.75
object_y = 340.25
velocity = 12.6
frame_time = 0.016
Незначительная погрешность обычно допустима, а вычисления выполняются очень часто.
24. Когда float лучше не использовать
float не следует использовать для величин, которые должны совпадать абсолютно точно.
Наиболее важный пример — деньги:
balance = 0.1 + 0.2
Для бухгалтерских, платёжных и финансовых расчётов обычно используют:
- целые числа с минимальными денежными единицами;
- класс
Decimal; - точные типы
DECIMALилиNUMERICв базе данных.
Например, сумму 1250.75 рубля можно хранить как:
amount_in_kopecks = 125_075
Или как:
from decimal import Decimal
amount = Decimal('1250.75')
Официальная документация Python рекомендует decimal для задач, которым требуется точное десятичное представление, включая бухгалтерские расчёты.
Вещественные числа в SQL
25. Проблема существует и в базах данных
SQL сам по себе не задаёт один универсальный способ хранения дробей.
В базах данных обычно существуют:
- приблизительные типы
FLOAT,REAL,DOUBLE PRECISION; - точные десятичные типы
DECIMAL,NUMERIC.
Например, PostgreSQL описывает double precision как восьмибайтовый неточный тип примерно с 15 десятичными цифрами точности. MySQL также указывает, что FLOAT и DOUBLE хранят приблизительные значения, а DECIMAL — точные десятичные.
26. Пример для PostgreSQL
Чтобы явно использовать вещественные числа двойной точности:
SELECT
0.1::double precision
+ 0.2::double precision;
Можно получить:
0.30000000000000004
Проверка:
SELECT
0.1::double precision
+ 0.2::double precision
= 0.3::double precision;
вернёт:
false
Но при использовании точного типа NUMERIC результат будет другим:
SELECT
0.1::numeric
+ 0.2::numeric
= 0.3::numeric;
Результат:
true
Поэтому проблема зависит не просто от использования SQL, а от выбранного типа данных.
Сравнение float
27. Почему == подходит не всегда
first_number = 0.3
second_number = 0.1 + 0.2
print(first_number == second_number)
Результат:
False
Для приблизительных вычислений часто требуется не абсолютное равенство, а проверка:
Достаточно ли близки два числа?
28. Находим разницу
first_number = 0.3
second_number = 0.1 + 0.2
difference = first_number - second_number
print(difference)
Результат будет отрицательным:
-5.551115123125783e-17
Нас интересует размер погрешности, а не её знак.
Поэтому используем abs():
difference = abs(first_number - second_number)
print(difference)
Результат:
5.551115123125783e-17
29. Допустимая погрешность
Создадим допустимую погрешность:
tolerance = 1e-15
Это число:
0.000000000000001
Теперь проверим, меньше ли разница допустимой погрешности:
first_number = 0.3
second_number = 0.1 + 0.2
tolerance = 1e-15
result = abs(first_number - second_number) < tolerance
print(result)
Результат:
True
Мы не утверждаем, что числа абсолютно равны.
Мы утверждаем:
Разница между числами настолько мала, что в нашей задаче её можно не учитывать.
30. Функция для сравнения
def are_floats_close(
first_number,
second_number,
tolerance,
):
difference = abs(first_number - second_number)
return difference < tolerance
Использование:
result = are_floats_close(
0.3,
0.1 + 0.2,
1e-15,
)
print(result)
Результат:
True
31. Погрешность выбирается под задачу
Не существует одного значения tolerance, которое идеально подходит абсолютно для всех программ.
Например:
tolerance = 1e-15
может быть разумным для одних небольших чисел, но не подходить для:
- очень больших значений;
- очень маленьких значений;
- измерений с другой допустимой точностью;
- финансовых расчётов.
Допустимую погрешность выбирают с учётом смысла данных.
Например, для температуры может быть достаточно:
tolerance = 0.1
А для технического коэффициента может потребоваться:
tolerance = 1e-9
32. Функция math.isclose()
В стандартном модуле math существует функция:
math.isclose()
Она предназначена для определения того, достаточно ли близки два числа.
from math import isclose
print(isclose(0.1 + 0.2, 0.3))
Результат:
True
Функция умеет учитывать:
- относительную погрешность;
- абсолютную погрешность.
Она будет подробно изучаться позже. Для настоящих программ обычно лучше использовать math.isclose(), чем каждый раз самостоятельно писать сравнение через abs(). Официальная документация Python также рекомендует её для сравнения неточных результатов.
33. Основные правила урока
Объекты класса float часто хранят приблизительное значение:
0.1 + 0.2
Результат:
0.30000000000000004
Это не ошибка Python, а особенность двоичной арифметики с плавающей точкой.
На большинстве платформ Python использует 64-битный формат двойной точности:
1 бит — знак
11 бит — экспонента
52 бита — записанная дробная часть
53 бита общей двоичной точности
Точность составляет примерно 15–17 значащих десятичных цифр, а не фиксированное количество цифр после точки.
float имеет фиксированный размер, огромный диапазон и высокую скорость, поэтому подходит для:
- погоды;
- координат;
- аналитики;
- статистики;
- машинного обучения;
- графики;
- физического моделирования.
Для денег и других точных десятичных расчётов обычно применяются Decimal или целые числа.
Сравнивать результаты вычислений через == нужно осторожно:
0.1 + 0.2 == 0.3
Для учебного сравнения можно использовать допустимую погрешность:
abs(first_number - second_number) < tolerance
В дальнейшем для этого будет использоваться:
math.isclose()
Материалы для дополнительного чтения
1. Официальная документация Python
Floating-Point Arithmetic: Issues and Limitations
Это основной материал по теме.
На первом прочтении достаточно понять:
- почему
0.1нельзя точно представить в двоичном виде; - почему
0.1 + 0.2может отличаться от0.3; - почему это не ошибка Python;
- зачем используется
math.isclose().
Необязательно сразу полностью разбирать:
- сложные дроби;
as_integer_ratio();- шестнадцатеричное представление;
Fraction;- точные алгоритмы суммирования.
Достаточно понять примерно 15–20% статьи и общий принцип.
2. Wikipedia
Материал значительно глубже и сложнее.
Можно обратить внимание на разделы:
- общая идея чисел с плавающей точкой;
- значащая часть и экспонента;
- IEEE 754;
- ошибки округления.
Математические детали можно пока пропускать.
3. Практические примеры проблем
Examples of floating point problems
Здесь собраны практические примеры неожиданного поведения вещественных чисел.
Материал подходит для знакомства с тем, какие ошибки могут появляться в настоящих программах.
4. Более подробный материал
Это дополнительный и более сложный материал.
Его стоит читать только при желании глубже разобраться:
- в представлении чисел;
- ошибках округления;
- накоплении погрешностей;
- особенностях математических операций.
Для прохождения текущей темы полностью разбирать его не требуется.
Домашнее задание
Задание 1. Сравнение с допустимой погрешностью
Создать функцию:
are_floats_close()
Функция принимает три обязательных позиционных аргумента:
first_number
second_number
tolerance
Внутри функции нужно:
- найти абсолютную разницу между числами;
- сравнить её с допустимой погрешностью;
- вернуть
TrueилиFalse.
Использовать:
abs()
Примеры вызовов:
print(
are_floats_close(
0.1 + 0.2,
0.3,
1e-15,
)
)
print(
are_floats_close(
10.5,
10.7,
0.1,
)
)
Ожидаемый результат:
True
False
Задание 2. Отчёт об измерении
Создать функцию:
create_measurement_report()
Функция принимает:
expected_value
actual_value
tolerance
Внутри функции нужно:
- найти абсолютную разницу;
- проверить, находится ли она в пределах допустимой погрешности;
- вернуть многострочную f-строку.
Пример результата:
Ожидаемое значение: 20.0
Фактическое значение: 20.000000000000004
Разница: 3.552713678800501e-15
Допустимая погрешность: 1e-14
Результат проверки: True
Для сравнения использовать:
abs(expected_value - actual_value) < tolerance