Почему числа с плавающей точкой хранятся с неточностью в памяти
Что проверяют
Понимаете ли вы, что такое двоичное представление вещественных чисел, и умеете ли из этого делать выводы в коде: не сравнивать через равенство, не хранить деньги во float, знать про decimal и math.isclose.
Как отвечать
Тип float в Python это двойная точность по IEEE 754: 64 бита, из них один на знак, одиннадцать на порядок и пятьдесят два на хранимую мантиссу, что вместе с подразумеваемой единицей даёт пятьдесят три значащих двоичных разряда. Никакого 32-битного типа в самом языке нет, он появляется только в сторонних библиотеках вроде numpy.
Число хранится как знак, мантисса и степень двойки, поэтому точно представимы лишь дроби со знаменателем в виде степени двойки. Десятичная одна десятая в двоичной системе периодическая, сохраняется ближайшее представимое значение, и сложение 0.1 + 0.2 даёт 0.30000000000000004. Увидеть реальное значение можно через (0.1).hex() или Decimal(0.1).
Выводы для практики:
- сравнение через
math.isclose(a, b, rel_tol=..., abs_tol=...), а не через==; - деньги в
decimal.Decimalс конструктором от строки либо в целых копейках; - точные дроби в
fractions.Fraction; - длинные суммы через
math.fsum, он компенсирует накопленную погрешность лучше обычногоsum.
Отдельно стоит упомянуть, что round работает с тем же представлением и округляет к ближайшему чётному, поэтому результат иногда расходится с ожиданиями школьной арифметики.
Где спотыкаются
Думают, что погрешность рождается при сложении. Она уже в литерале: 0.1 не равно одной десятой ещё до всякой арифметики, а операции лишь делают расхождение заметным.
Лечат выводом. Человек видит длинный хвост, оборачивает значение в round до двух знаков и считает вопрос закрытым. Округление для печати не делает корректным ни сравнение, ни денежные расчёты.
Строят Decimal поверх float. Запись Decimal(0.1) наследует двоичную ошибку целиком, в конструктор нужно передавать строку. Из той же серии утверждение про 32 бита: путают тип языка и float32 из numpy.