Почему 0.1 + 0.2 не равно 0.3?
- Как устроен тип double?
- Что такое IEEE 754?
- Как правильно сравнивать числа с плавающей точкой?
double хранит число как двоичную дробь: знак, порядок и 52 бита мантиссы. 0.1 в двоичной системе бесконечна, как 1/3 в десятичной, — хранится приближение.
- 0.1 + 0.2 = 0.30000000000000004: ошибки округления сложились
- Сравнивать с допуском: |a − b| < ε, лучше относительным
- Точно представимы целые до 2⁵³ и дроби вида k/2ⁿ
А ещё Спецзначения: NaN ≠ NaN, +0 и −0, бесконечности
Подробно
Тип double (IEEE 754, 64 бита) хранит число в научной записи, только по основанию 2: знак (1 бит), порядок (11 бит, со смещением 1023) и мантисса (52 бита, плюс неявная ведущая единица). Значение — (−1)^знак × 1.мантисса × 2^(порядок − 1023).
Конечной двоичной дробью записываются только числа вида k/2ⁿ: 0.5, 0.25, 0.375. А 0.1 = 1/10, и в знаменателе есть пятёрка — в двоичной системе получается бесконечная периодическая дробь 0.000110011001100… Её обрезают до 53 значащих бит, и хранится ближайшее представимое число — 0.1000000000000000055511… То же с 0.2. При сложении ошибки складываются, результат округляется до ближайшего double, и это не тот double, который получается из литерала 0.3.
Сколько точности. 53 бита — это 15–17 значащих десятичных цифр. Все целые до 2⁵³ (≈ 9·10¹⁵) представимы точно, дальше — уже через одно. У float 24 бита мантиссы — около 7 цифр, и 16 777 217 в него не помещается.
Сравнение. == для вычисленных значений почти всегда ошибка. Сравнивают с допуском: абсолютным abs(a - b) < 1e-9 для чисел около единицы или относительным, как math.isclose, для чисел любого масштаба.
Спецзначения. +∞ и −∞ (1.0 / 0 в Java), NaN (0.0 / 0, корень из −1) — и NaN не равен ничему, даже себе, поэтому проверяют isnan. Есть −0.0, равный +0.0, но 1 / −0.0 = −∞.
Последствия: порядок сложения меняет результат (сумма большого числа малых теряет точность — помогает алгоритм Кэхэна или math.fsum), деньги в double не хранят, счётчик цикла с шагом 0.1 не дойдёт ровно до 1.0.
Пример
0.1 + 0.2 # 0.30000000000000004
0.1 + 0.2 == 0.3 # False
import math
math.isclose(0.1 + 0.2, 0.3) # True
from decimal import Decimal
Decimal(0.1) # 0.1000000000000000055511151231257827…
2.0 ** 53 + 1 == 2.0 ** 53 # True — дальше целые уже не все
float("nan") == float("nan") # FalseЧто хотят услышать
- Двоичное представление: 0.1 нельзя записать конечной двоичной дробью.
- Устройство: знак, порядок, мантисса, примерно 15–17 цифр точности.
- Сравнение с допуском, лучше относительным.
- Следствие для денег — хранить не в double.
- NaN и бесконечности — плюс.
Частые ошибки
- «Компьютер считает неточно» без объяснения про двоичные дроби.
- Сравнивать через
==или с одним абсолютным ε для чисел любого масштаба.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✓0.1 в двоичной системе — бесконечная дробь, хранится приближение; сравнивать числа нужно с допуском
- ✗Ошибка округления в конкретном языке: в языках с типом decimal по умолчанию, например Java и Go, 0.1 + 0.2 точно равно 0.3
- ✗double хранит числа в десятичном виде
- ✗NaN == NaN возвращает true
Что спросят дальше
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.