Перейти к содержимому
Теория · Операторы · урок 3 из 9

Числа с плавающей точкой

Попробуйте записать одну треть десятичной дробью на бумаге. Получится 0.333…, и тройки никогда не кончатся. Где-то придётся остановиться, например на 0.333. Сложите три таких числа — выйдет 0.999, а не единица.

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

Python
price = 0.1
fee = 0.2
total = price + fee
print(total)  # 0.30000000000000004
print(total == 0.3)  # False

Давайте разберём, что здесь произошло:

  1. Первые две строки кладут в переменные 0.1 и 0.2.
  2. total = price + fee складывает их.
  3. print(total) печатает 0.30000000000000004 — с лишней четвёркой в конце.
  4. print(total == 0.3) печатает False.

Оператор == сравнивает два значения: он даёт True, если они равны, и False, если нет. Здесь он дал False, потому что в последней цифре суммы набежала ошибка.

Как хранится дробь

Такие числа называют числами с плавающей точкой. Они хранят около 16 значащих цифр, а отдельный множитель задаёт, где стоит точка. Поэтому float вмещает и 0.000001, и триллионы, но почти любая дробь в нём приближённая.

В памяти лежит не 0.1, а ближайшее к нему двоичное число, и то же с 0.2. Две крошечные погрешности округления сложились и стали видны. print показывает самую короткую запись, которая даёт то же число, поэтому print(0.1) выводит 0.1 без длинного хвоста.

У аналогии с третью есть предел: в двоичной системе неудобные дроби другие. 0.5 и 0.25 хранятся точно, и 0.5 + 0.25 даёт ровно 0.75. А 0.1 в двоичном виде — бесконечная дробь, как треть в десятичном.

Этот вопрос часто задают на собеседовании: «Почему 0.1 + 0.2 не равно 0.3?» Хороший ответ называет двоичное хранение, погрешность округления и способ сравнивать такие числа.

Целые числа типа int хранятся точно, сколько бы в них ни было цифр. Поэтому суммы денег считают в копейках или центах, целыми числами.

Округление и сравнение

Сравнивать дробные числа через == ненадёжно. В Python для этого есть два инструмента.

Функция round

Функция round(x, n) округляет дробное число x до n знаков после точки и возвращает float. Без второго аргумента она возвращает ближайшее целое число типа int:

Python
total = 0.1 + 0.2
print(round(total, 2))  # 0.3
print(round(total, 2) == 0.3)  # True
print(round(3.7), round(-3.7))  # 4 -4

Разберём по строкам:

  1. round(total, 2) печатает 0.3: хвост из нулей и четвёрки ушёл.
  2. Сравнение округлённой суммы с 0.3 даёт True.
  3. round(3.7) и round(-3.7) дают целые 4 и -4.

Округлённая сумма — то же ближайшее к 0.3 двоичное число, что получается из записи 0.3 в коде. Поэтому второе сравнение верно.

Функция math.isclose

Округлять перед каждым сравнением неудобно: нужно угадать число знаков. Функция math.isclose отвечает, равны ли два числа с учётом крошечной погрешности. Строка import math в начале программы подключает модуль math — готовый набор математических функций, и isclose — одна из них:

Python
import math

print(math.isclose(0.1 + 0.2, 0.3))  # True
print(math.isclose(0.3, 0.31))  # False

isclose считает числа равными, если они различаются не больше чем на миллиардную долю большего из них. Поэтому 0.1 + 0.2 и 0.3 для неё равны, а 0.3 и 0.31 — нет. Дробные ответы в задачах курса проверяются так же.

Частая ошибка

Ждут, что round округляет половину вверх, как в школе:

Python
price = 2.5
rounded = round(price)  # ✗
print(rounded)  # 2

Ошибки нет, но результат — 2, а не 3. Python округляет ровную половину к ближайшему чётному числу: round(1.5) и round(2.5) оба дают 2. Так при многих округлениях ошибки вверх и вниз уравновешивают друг друга. Для положительного числа половину вверх даёт int(price + 0.5), то есть 3.

Главное

  • float хранит дробь в двоичном виде приближённо, поэтому 0.1 + 0.2 печатает 0.30000000000000004.
  • Вычисленные дробные числа не сравнивают через ==: берут round или math.isclose.
  • round округляет ровную половину к чётному: round(2.5) даёт 2.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом