Компьютер, конечно, совсем не математик!
Иллюстрация 1. Главный герой нашей истории — неожиданная четвёрка в конце ответа.
Почему 0,1 + 0,2 иногда не равно 0,3, откуда берётся загадочная четвёрка и почему эта мелочь важна даже взрослым программистам.
Представьте: человечество научило машины распознавать лица, строить маршруты, моделировать погоду и даже писать стихи. А теперь мы открываем Python и просим его сделать нечто совершенно непосильное — сложить две дроби.
>>> 0.1 + 0.2
0.30000000000000004
Так. С математикой у нас, кажется, небольшая проблема. Первоклассник сказал бы «0,3», а компьютер зачем-то дописал в конце четвёрку. Может, он решил немного приукрасить ответ? Или это тот самый момент, когда искусственный интеллект захватывает мир, но всё ещё не сдал контрольную?
Спойлер: компьютер не ошибся в сложении в обычном смысле. Он выполнил операцию над числами, которые хранятся в памяти не совсем так, как мы их себе представляем. Чтобы разобраться, придётся ненадолго заглянуть внутрь числа.
1. Почему 0,1 — это не всегда 0,1
Мы привыкли к десятичной системе: десять цифр, разряды единиц, десятков, сотен и так далее. Но дробная часть тоже состоит из разрядов. Например, 0,125 — это одна десятая, две сотых и пять тысячных.
В двоичной системе всё устроено аналогично, только каждый следующий разряд после запятой означает не одну десятую, а половину; затем четверть; затем одну восьмую и так далее.
0,1₂ = 1/2 = 0,5₁₀
0,01₂ = 1/4 = 0,25₁₀
0,001₂ = 1/8 = 0,125₁₀
Вот тут и начинается магия: 0,5 прекрасно представляется двоичной дробью, а десятичное 0,1 — нет. Чтобы записать одну десятую в двоичной системе, приходится повторять последовательность цифр бесконечно.
Иллюстрация 2. Одна десятая выглядит коротко в десятичной записи и бесконечно — в двоичной.
Знакомая ситуация? Конечно! Вспомните одну треть: 1/3 = 0,333333… Мы можем написать сколько угодно троек, но конечная запись всё равно будет приближением. С одной десятой в двоичной системе происходит то же самое.
Иллюстрация 3. Точность представления зависит от системы счисления, а не от «простоты» числа на вид.
2. Компьютер не может хранить бесконечность
Компьютерная память конечна. Для хранения чисел с дробной частью часто используют формат с плавающей точкой. На обычных современных платформах Python тип float основан на 64-битном двоичном формате binary64, описанном стандартом IEEE 754.
У этого формата 64 бита: один отвечает за знак, одиннадцать хранят закодированный порядок, ещё пятьдесят два — дробную часть значащего числа. У нормализованных чисел есть ещё неявная ведущая единица, поэтому фактическая точность составляет 53 двоичных значащих разряда.
Иллюстрация 4. Упрощённая схема структуры binary64. Реальные правила включают специальные значения и исключения.
Звучит сложно? Можно представить маленький чемодан для числа. В него помещается определённое количество цифр. Если двоичная дробь бесконечная, её приходится округлить до ближайшего значения, которое в этот чемодан влезает.
И вот важный поворот: когда мы пишем 0.1 в коде, компьютер обычно получает не идеальную математическую одну десятую, а ближайшее доступное двоичное значение.
3. Откуда взялась эта злополучная четвёрка?
Итак, оба исходных числа — 0,1 и 0,2 — уже представлены приближённо. Компьютер складывает эти приближения, а затем округляет результат по правилам арифметики с плавающей точкой.
Получается число, которое чуть-чуть больше ближайшего представимого значения к математически точному 0,3. Python показывает кратчайшую десятичную запись, которая однозначно восстанавливает получившееся значение float. Отсюда и хвост:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Здесь легко сделать неверный вывод: «Компьютер плохо считает». Точнее будет сказать иначе: мы попросили систему конечной точности выполнить операцию над числами, которые нельзя точно сохранить в выбранном формате. И получили корректный для этого формата результат.
4. Но почему Python иногда показывает просто 0.1?
Проверим:
>>> print(0.1)
0.1
>>> format(0.1, ".20f")
'0.10000000000000000555'
Неужели Python сначала скрывал правду? Нет. Для обычного вывода он выбирает короткую и удобную десятичную запись, которая при обратном чтении даёт то же самое машинное число. Но если попросить показать больше цифр, приближение становится заметнее.
Можно даже увидеть точное отношение целых чисел, соответствующее сохранённому float:
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
Это и есть конкретное двоичное приближение к одной десятой. С виду страшновато, но математика здесь честная.
5. Почему с 0,5 всё работает идеально?
Потому что некоторые дроби действительно представимы в двоичной системе без остатка. Например, половина, четверть и одна восьмая — это степени двойки. Их можно записать конечной последовательностью битов.
>>> 0.5 + 0.25 == 0.75
True
>>> 0.1 + 0.2 == 0.3
False
Иллюстрация 5. Разные ответы не противоречат друг другу: одни дроби представлены точно, другие — нет.
Ключевое правило: рациональная дробь после сокращения имеет конечную двоичную запись, если её знаменатель — степень двойки. Поэтому 1/8 записывается точно, а 1/10 — нет.
6. Значит, между числами в компьютере есть пустоты?
Да, если говорить о числах конкретного формата float. Математическая числовая прямая непрерывна, а набор представимых машинных чисел конечен. Между соседними значениями float существуют промежутки — числа, которые этот формат не умеет хранить точно.
Причём расстояние между соседними представимыми числами меняется в зависимости от масштаба. Для очень больших значений шаг между соседями может оказаться гораздо крупнее, чем для чисел около единицы.
Иллюстрация 6. Схематическая «решётка» представимых чисел. Расстояния на рисунке условные.
Поэтому дело не только в десятичных дробях. Например, очень большие целые числа тоже могут перестать различаться при переводе в float:
>>> float(2**53) == float(2**53 + 1)
True
Это не значит, что Python «не умеет большие числа»: обычный тип int в Python поддерживает целые числа произвольной точности в пределах доступной памяти. Проблема появляется именно при переходе к float.
7. А в реальной жизни это опасно?
Одна лишняя четвёрка в конце ответа обычно не приведёт к катастрофе. Для компьютерной графики, моделирования физических процессов и множества инженерных задач небольшие контролируемые погрешности допустимы. Важно не отсутствие любого отклонения, а понимание требуемой точности.
Но представьте финансовую систему, где разработчик бездумно проверяет точное равенство дробных результатов. Или научную программу, где множество операций усиливает погрешность. Здесь уже важно правильно выбрать тип данных, алгоритм и способ сравнения.
При этом стоит избегать другой крайности: арифметика с плавающей точкой не «плохая» и не «сломанная». Это невероятно полезный инженерный компромисс между скоростью, диапазоном значений и точностью.
8. Как программисты с этим живут?
Приём № 1. Сравнивать с разумной погрешностью
Если вы работаете с приближёнными значениями, часто не стоит требовать строгого совпадения всех битов. В Python есть функция math.isclose:
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
Но здесь есть нюанс: «близко» зависит от задачи. Функция использует относительную и абсолютную погрешности, которые можно настроить. Для значений около нуля абсолютный допуск особенно важен.
Приём № 2. Использовать десятичные числа
Если принципиально важно хранить десятичные значения и выполнять операции в десятичной арифметике, Python предлагает модуль decimal:
from decimal import Decimal
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b) # 0.3
Кавычки здесь не случайны: если передать в Decimal уже округлённое значение float, вы перенесёте исходное приближение. И помните, что decimal тоже работает с конечной точностью: например, 1/3 нельзя точно записать и в десятичном виде.
Приём № 3. Считать в целых единицах
Иногда удобнее хранить денежные суммы в копейках, расстояния — в миллиметрах, время — в миллисекундах. Если задача допускает фиксированную минимальную единицу, многие операции можно выполнять над целыми числами.
price_kopecks = 1050 # 10 рублей 50 копеек
quantity = 3
print(price_kopecks * quantity) # 3150 копеек
Но и этот подход не отменяет необходимость правильно рассчитывать проценты, налоги и округления. Выбор инструмента определяется задачей, а не универсальным правилом «float нельзя использовать».
Иллюстрация 7. Три полезных подхода — каждый для своего класса задач.
9. Попробуйте поймать компьютер на «ошибке»
Вот три небольших эксперимента, которые можно выполнить в Python самостоятельно. Они займут меньше минуты, но помогут закрепить идею.
print(0.1 + 0.2 == 0.3)
print(0.5 + 0.25 == 0.75)
print(0.1 + 0.1 + 0.1 == 0.3)
А теперь попробуйте объяснить каждый ответ, не подглядывая в статью. Почему одно равенство истинно, а другое — нет? Что меняется, когда мы складываем три одинаковых приближения?
Дополнительное задание для любопытных: посмотрите, что возвращает math.ulp(1.0), и выясните, какова разница между единицей и следующим представимым float.
10. Главный вывод — компьютер не глупый, просто он буквальный
Мы воспринимаем 0,1 как точное математическое число. Компьютер в формате float оперирует одним из конечного набора двоичных значений. В большинстве ситуаций разница ничтожна, но иногда становится видна — например, в знаменитом выражении 0.1 + 0.2.
Пожалуй, в этом и состоит один из самых интересных уроков информатики: даже привычное действие вроде сложения зависит от того, как мы представили данные. А значит, чтобы доверять результату, полезно понимать устройство инструмента.
Так что в следующий раз, когда Python выдаст 0.30000000000000004, не ругайте его. Он просто слишком честно показал вам особенности собственной памяти.
△ Умный Чатик
Источники и материалы для самостоятельного чтения
Python Documentation — Floating Point Arithmetic: Issues and Limitations: https://docs.python.org/3/tutorial/floatingpoint.html
Python Documentation — decimal: https://docs.python.org/3/library/decimal.html
Python Documentation — math.isclose и math.ulp: https://docs.python.org/3/library/math.html
IEEE 754 — IEEE Standard for Floating-Point Arithmetic (официальный стандарт; полный текст может быть платным): https://standards.ieee.org/ieee/754/6210/







