Прямой ответ
Decimal хранит десятичное значение точно, float представляет часть десятичных дробей приближённо, поэтому для денежных сумм выбирается Decimal.
| Критерий | Decimal | float |
|---|---|---|
| Представление | Десятичная дробь точно | Двоичная дробь приближённо |
| Погрешность | Отсутствует в пределах точности | Присутствует в последних разрядах |
| Накопление | Не накапливается | Накапливается при операциях |
| Скорость | Ниже | Выше |
| Область | Деньги, отчётность | Научные и статистические расчёты |
from decimal import Decimal
0.1 + 0.2 # 0.30000000000000004
Decimal("0.1") + Decimal("0.2") # Decimal("0.3")
Сложение в двоичном типе даёт значение с погрешностью в последних разрядах, поэтому сравнение с ожидаемым результатом не совпадает. О хранении денег — в статье про деньги в приложении.
Представление значения и точность
Способ представления значения определяет, сохраняется ли десятичная дробь точно, поэтому погрешность зависит от типа.
Десятичное представление. Decimal хранит значение как десятичную дробь с фиксированной точностью, поэтому сумма в разменных единицах представляется без потери разрядов.
Двоичное представление. float хранит значение как двоичную дробь, поэтому часть десятичных дробей не представляется точно и округляется до ближайшего двоичного значения.
Видимость погрешности. Погрешность двоичного представления видна при выводе значения, поэтому результат операции отличается от ожидаемого в последних разрядах.
Практическое следствие: сравнение результата арифметики в двоичном типе с ожидаемым значением не совпадает, поэтому проверка равенства требует допуска.
Отдельный аспект — точность по умолчанию. Десятичный тип имеет настройку точности, определяющую число значащих разрядов, поэтому значение за пределами точности округляется. Точность задаётся явно для денежного расчёта, чтобы разрядность покрывала минимальную единицу валюты.
Накопление погрешности при арифметике
Погрешность накапливается при последовательных операциях, поэтому итог по большому числу значений расходится с ожидаемым сильнее, чем погрешность одной операции.
Погрешность одной операции. Одна операция в двоичном типе даёт расхождение в последних разрядах, поэтому значение отличается от точного на малую величину.
Накопление при сложении. Последовательное сложение значений переносит погрешность в следующий шаг, поэтому расхождение растёт с числом операций.
Итог по набору. Сумма по большому числу значений расходится с точным итогом на величину, заметную в разменных единицах, поэтому расчёт не сходится с отчётностью.
Практическое следствие: итог по тысяче позиций, посчитанный в двоичном типе, расходится с ожидаемым на несколько разменных единиц, поэтому сверка с выпиской показывает расхождение.
Скорость и область применения
Скорость вычисления в двоичном типе выше, поэтому выбор типа зависит от требования к точности и объёма вычислений.
Скорость Decimal. Десятичная арифметика выполняется медленнее двоичной, поэтому объёмный расчёт занимает больше времени.
Скорость float. Двоичная арифметика выполняется аппаратно, поэтому расчёт большого объёма данных выполняется быстрее.
Область применения. Денежные суммы и отчётность требуют точного представления, статистические и научные расчёты допускают погрешность в последних разрядах.
Практическое следствие: расчёт метрик по большому набору данных в двоичном типе выполняется быстрее, поэтому погрешность в последних разрядах приемлема для аналитики, но не для денежных сумм.
Отдельный аспект — смешение типов. Операция между десятичным и двоичным значением отклоняется либо требует явного преобразования, поэтому неявное смешение типов исключается. Преобразование двоичного значения в десятичное фиксирует погрешность представления, поэтому преобразование выполняется от строкового представления, а не от двоичного значения.
Сравнение по задачам
Выбор типа определяется задачей, поэтому денежный расчёт и аналитический расчёт используют разные типы.
Денежный расчёт. Сумма, налог и скидка требуют точного представления, поэтому используется десятичный тип либо целое число минимальных единиц.
Аналитический расчёт. Среднее, доля и коэффициент допускают погрешность в последних разрядах, поэтому двоичный тип приемлем.
Граничный случай. Расчёт, результат которого попадает в отчётность, выполняется в десятичном типе, поэтому погрешность не переносится в документ.
Практическое следствие: расчёт, начинающийся как аналитический и попадающий в отчётность, требует перехода на десятичный тип до формирования документа.
О правиле округления — в статье про округление денежных значений; о типах данных в моделях — в статье про Pydantic и dataclasses.
Практика: выбор типа для денежного расчёта
1. Используйте Decimal для денежных сумм. Значение представляется точно, поэтому итог совпадает с ожидаемым.
2. Создавайте Decimal из строки. Создание из двоичного значения фиксирует погрешность представления в десятичном типе.
3. Задавайте точность явно. Число значащих разрядов должно покрывать минимальную единицу валюты.
4. Не смешивайте Decimal и float в операции. Операция отклоняется либо требует явного преобразования.
5. Используйте float для аналитических метрик. Погрешность в последних разрядах приемлема для среднего и доли.
6. Переходите на Decimal перед формированием отчётности. Значение в документе не содержит погрешности представления.
7. Проверяйте итог контрольной суммой. Расхождение указывает на погрешность типа либо на неверное правило округления.
Рекомендации:
- Не сравнивайте результат float на равенство. Погрешность в последних разрядах делает сравнение несовпадающим, поэтому требуется допуск.
- Учитывайте накопление погрешности при большом числе операций. Итог расходится сильнее, чем погрешность одной операции.
- Сопоставляйте скорость с объёмом расчёта. Десятичная арифметика медленнее, поэтому объёмный расчёт занимает больше времени.
- Фиксируйте тип в схеме данных. Смешение типов в одном поле приводит к неявному преобразованию и погрешности.
- Пересматриваем тип при изменении назначения расчёта. Попадание результата в отчётность требует десятичного типа.
Практический сценарий: сверка расчёта с бухгалтерским отчётом не сходилась
Контекст: сумма начислений за месяц в приложении отличалась от суммы в бухгалтерском отчёте на несколько рублей, при этом состав операций совпадал.
- Диагностика. Проверка показала, что сумма каждой операции хранилась в двоичном типе. Отдельная операция давала расхождение в последних разрядах, а сложение по нескольким тысячам операций накопило расхождение до нескольких рублей.
- Причина. Двоичное представление не хранит часть десятичных дробей точно, поэтому погрешность каждой операции переносилась в следующий шаг сложения и накапливалась.
- Действия. (а) Заменили хранение суммы операции на десятичный тип, созданный из строкового представления, чтобы погрешность двоичного значения не фиксировалась; (б) задали точность десятичного типа явно, чтобы разрядность покрывала минимальную единицу валюты; (в) перенесли округление в конец расчёта, чтобы промежуточные значения сохраняли точность; (г) пересчитали месяц и сравнили с бухгалтерским отчётом, убедившись в совпадении; (д) добавили контрольную сумму по периоду, чтобы расхождение обнаруживалось автоматически.
- Результат. Сумма за период совпадает с бухгалтерским отчётом, поскольку арифметика выполняется без погрешности представления и накопления.
Вывод: погрешность двоичного представления накапливается при последовательных операциях, поэтому итог по большому числу значений расходится с отчётностью. Десятичный тип, созданный из строки, и округление после полного расчёта устраняют накопление погрешности.
FAQ
Почему float не подходит для денег? Двоичное представление не хранит часть десятичных дробей точно, поэтому арифметика вносит погрешность, накапливающуюся при последовательных операциях.
Можно ли создать Decimal из float? Технически да, но такое создание фиксирует погрешность двоичного значения в десятичном типе, поэтому Decimal создаётся из строки.
Где float остаётся уместным? В аналитических и статистических расчётах, где погрешность в последних разрядах приемлема и не переносится в отчётность.
Что быстрее: Decimal или float? Двоичная арифметика выполняется аппаратно и быстрее, поэтому объёмный расчёт в десятичном типе занимает больше времени.
Источники: Python decimal, Python floating point, PostgreSQL numeric types — ссылки в шапке.