Числа с плавающей точкой
Попробуйте записать одну треть десятичной дробью на бумаге. Получится 0.333…, и тройки никогда не кончатся. Где-то придётся остановиться, например на 0.333. Сложите три таких числа — выйдет 0.999, а не единица.
Компьютер попадает в ту же ловушку, только считает в двоичной системе. Дробные числа типа float хранятся в двоичном виде, а большинство десятичных дробей так точно не записать. Поэтому сумма двух коротких дробей может отличаться от той, что получилась бы на бумаге:
price = 0.1
fee = 0.2
total = price + fee
print(total) # 0.30000000000000004
print(total == 0.3) # FalseДавайте разберём, что здесь произошло:
- Первые две строки кладут в переменные
0.1и0.2. total = price + feeскладывает их.print(total)печатает0.30000000000000004— с лишней четвёркой в конце.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:
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Разберём по строкам:
round(total, 2)печатает0.3: хвост из нулей и четвёрки ушёл.- Сравнение округлённой суммы с
0.3даётTrue. round(3.7)иround(-3.7)дают целые4и-4.
Округлённая сумма — то же ближайшее к 0.3 двоичное число, что получается из записи 0.3 в коде. Поэтому второе сравнение верно.
Функция math.isclose
Округлять перед каждым сравнением неудобно: нужно угадать число знаков. Функция math.isclose отвечает, равны ли два числа с учётом крошечной погрешности. Строка import math в начале программы подключает модуль math — готовый набор математических функций, и isclose — одна из них:
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
print(math.isclose(0.3, 0.31)) # Falseisclose считает числа равными, если они различаются не больше чем на миллиардную долю большего из них. Поэтому 0.1 + 0.2 и 0.3 для неё равны, а 0.3 и 0.31 — нет. Дробные ответы в задачах курса проверяются так же.
Частая ошибка
Ждут, что round округляет половину вверх, как в школе:
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 и подпишись на канал — откроются все уроки.