Округление чисел в Python: от round() до Decimal

Округление в Python кажется простой задачей ровно до первого сюрприза: round(2.5) возвращает 2, а round(2.675, 2) дает 2.67 вместо ожидаемых 2.68. Ни то, ни другое не баг. Первое - сознательно выбранное банковское округление, второе - следствие того, как хранятся числа с плавающей точкой.
Ниже разобраны все способы округления: функция round, функции модуля math, форматирование при выводе и модуль decimal для денег. Для каждого показано, когда его применять и где он подводит.
Почему 0.1 + 0.2 не равно 0.3
Прежде чем округлять, нужно понять, что именно мы округляем. Тип float хранит число в двоичной системе по стандарту IEEE 754. Многие привычные десятичные дроби, например 0.1, в двоичной системе бесконечны, как 1/3 в десятичной. Компьютер хранит ближайшее представимое значение.
print(0.1 + 0.2) # -> 0.30000000000000004
print(0.1 + 0.2 == 0.3) # -> False
from decimal import Decimal
print(Decimal(0.1))
# -> 0.1000000000000000055511151231257827021181583404541015625
Decimal(0.1) показывает точное значение, которое на самом деле лежит в переменной. Python при печати выводит самую короткую строку, которая однозначно задает это значение, поэтому обычно мы видим аккуратное 0.1. Ошибки накапливаются при арифметике и становятся видны.
Отсюда первое практическое правило: никогда не сравнивайте float через ==. Используйте math.isclose:
import math
print(math.isclose(0.1 + 0.2, 0.3)) # -> True
Округление через round(x, 2) == 0.3 тоже иногда работает, но это костыль. Подробнее об устройстве вещественных чисел рассказано в статье float в Python.
Функция round и банковское округление
Встроенная функция round(number, ndigits=None) округляет до заданного числа знаков после запятой. Без второго аргумента она возвращает int, с ним - тот же тип, что и у входного числа.
print(round(3.14159)) # -> 3 (int)
print(round(3.14159, 2)) # -> 3.14 (float)
print(round(3.7, 0)) # -> 4.0 (float, а не int)
print(round(1234.5678, -2)) # -> 1200.0 (отрицательные знаки - до сотен)
Отрицательный ndigits округляет до десятков, сотен, тысяч. Это удобно для отчетов: round(87_450, -3) дает 87000.
Почему round(2.5) равно 2
В школе учат: пятерка округляется вверх. Python 3 поступает иначе: если число ровно посередине между двумя кандидатами, выбирается четный. Это называется банковским округлением, или округлением половины к четному (round half to even).
print([round(x) for x in (0.5, 1.5, 2.5, 3.5, 4.5, -2.5)])
# -> [0, 2, 2, 4, 4, -2]
Зачем так сделано? При школьном правиле все "половинки" сдвигаются в одну сторону, и при суммировании большого количества округленных чисел накапливается систематическая ошибка. При округлении к четному половинки уходят то вверх, то вниз, и в среднем ошибка компенсируется. Такой режим предписывает стандарт IEEE 754, его же используют в бухгалтерии и статистике.
Почему round(2.675, 2) дает 2.67
Здесь банковское правило ни при чем. Число 2.675 нельзя точно записать в float, и реально хранится значение чуть меньше:
print(round(2.675, 2)) # -> 2.67
print(Decimal(2.675))
# -> 2.67499999999999982236431605997495353221893310546875
Это не половинка, а число меньше половинки, поэтому оно честно округляется вниз. round работает правильно, просто на вход пришло не то число, которое вы написали в коде. Если такой результат недопустим, нужен Decimal, о нем ниже.
Округление вверх и вниз: math.ceil, floor и trunc
Модуль math дает три функции, которые округляют до целого по направлению, а не до ближайшего:
math.floor(x)- вниз, к минус бесконечности (пол);math.ceil(x)- вверх, к плюс бесконечности (потолок);math.trunc(x)- отбрасывает дробную часть, то есть округляет к нулю.
import math
for x in (2.3, 2.7, -2.3, -2.7):
print(x, math.floor(x), math.ceil(x), math.trunc(x), int(x), round(x))
# -> 2.3 2 3 2 2 2
# -> 2.7 2 3 2 2 3
# -> -2.3 -3 -2 -2 -2 -2
# -> -2.7 -3 -2 -2 -2 -3
Для положительных чисел floor и trunc совпадают, а для отрицательных расходятся: floor(-2.3) равно -3, а trunc(-2.3) равно -2. Функция int() ведет себя как trunc. Все три функции возвращают int. Остальные функции модуля описаны в статье модуль math.
Сводная таблица способов округления
| Способ | Правило | 2.5 | -2.5 | Тип результата |
|---|---|---|---|---|
round(x) | к ближайшему, половина к четному | 2 | -2 | int |
math.floor(x) | вниз | 2 | -3 | int |
math.ceil(x) | вверх | 3 | -2 | int |
math.trunc(x), int(x) | к нулю | 2 | -2 | int |
Decimal + ROUND_HALF_UP | к ближайшему, половина от нуля | 3 | -3 | Decimal |
f"{x:.0f}" | к ближайшему, половина к четному | "2" | "-2" | str |
Округление вверх при делении целых
Частая задача: сколько страниц нужно для 101 записи, если на странице 20? Нужен потолок от деления. Можно написать math.ceil(101 / 20), но для больших целых надежнее остаться в целых числах и не связываться с float:
items, per_page = 101, 20
print(math.ceil(items / per_page)) # -> 6
print(-(-items // per_page)) # -> 6, только целочисленная арифметика
print((items + per_page - 1) // per_page) # -> 6
Оператор // всегда округляет вниз, к минус бесконечности: -7 // 2 равно -4, а не -3. Трюк с двойным минусом превращает пол в потолок.
Округление до 2 знаков после запятой
Самый частый запрос - округлить до двух знаков. Тут важно различать две задачи: изменить само число или только показать его красиво.
Форматирование при выводе: f"{x:.2f}"
Если число нужно только напечатать, округлять его не надо. Используйте спецификатор формата: значение в переменной останется точным, а на экран выйдет нужное количество знаков.
price = 1234.5
ratio = 0.1 + 0.2
print(f"{price:.2f}") # -> 1234.50 (дополнит нулями)
print(f"{ratio:.2f}") # -> 0.30
print(f"{price:,.2f}") # -> 1,234.50
print(f"{price:_.2f}".replace("_", " ")) # -> 1 234.50
print(f"{0.4567:.1%}") # -> 45.7%
В отличие от round, форматирование всегда выводит ровно указанное число знаков: round(1234.5, 2) напечатается как 1234.5, а f"{1234.5:.2f}" как 1234.50. Для отчетов и интерфейсов почти всегда нужен второй вариант. Правило округления у форматирования то же, что у round: f"{2.675:.2f}" даст 2.67. Все возможности спецификаторов разобраны в статье f-строки в Python.
Округление вверх до двух знаков и ловушка умножения
Стандартный рецепт округления вверх до сотых - умножить, взять потолок, разделить. Но он ломается на float:
x = 1.1
print(x * 100) # -> 110.00000000000001
print(math.ceil(x * 100) / 100) # -> 1.11 (ожидали 1.1)
Умножение добавило к числу крошечный хвост, и потолок отправил его на следующую сотую. Для таких задач правильный инструмент - Decimal с режимом ROUND_CEILING.
Модуль decimal: точное десятичное округление
Decimal хранит числа в десятичной системе, поэтому 0.1 в нем - это ровно 0.1. Ключевое правило: создавайте Decimal из строки, а не из float. Из float вы точно скопируете уже испорченное значение.
from decimal import Decimal, ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
print(Decimal("0.1") + Decimal("0.2")) # -> 0.3
print(Decimal(2.675)) # -> 2.67499999999999982236431605997495353221893310546875
print(Decimal("2.675")) # -> 2.675
Метод quantize и ROUND_HALF_UP
Округление в decimal делается методом quantize. Ему передают образец с нужным количеством знаков и режим округления:
CENT = Decimal("0.01")
def round_money(value: str | Decimal) -> Decimal:
return Decimal(value).quantize(CENT, rounding=ROUND_HALF_UP)
print(round_money("2.675")) # -> 2.68
print(round_money("2.665")) # -> 2.67
print(round_money("-2.675")) # -> -2.68
print(Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)) # -> 3
print(Decimal("1.1").quantize(CENT, rounding=ROUND_CEILING)) # -> 1.10
print(Decimal("1.101").quantize(CENT, rounding=ROUND_CEILING)) # -> 1.11
ROUND_HALF_UP - это то самое школьное округление: половина уходит от нуля, поэтому -2.675 становится -2.68. Если на входе float, сначала превратите его в строку: Decimal(str(2.675)) даст Decimal('2.675'), потому что str() возвращает короткое представление.
Основные режимы округления decimal
| Режим | Описание | 2.5 | -2.5 | 2.1 |
|---|---|---|---|---|
ROUND_HALF_UP | школьное, половина от нуля | 3 | -3 | 2 |
ROUND_HALF_EVEN | банковское (режим по умолчанию) | 2 | -2 | 2 |
ROUND_HALF_DOWN | половина к нулю | 2 | -2 | 2 |
ROUND_CEILING | вверх, к плюс бесконечности | 3 | -2 | 3 |
ROUND_FLOOR | вниз, к минус бесконечности | 2 | -3 | 2 |
ROUND_UP | от нуля | 3 | -3 | 3 |
ROUND_DOWN | к нулю (отбросить хвост) | 2 | -2 | 2 |
Обратите внимание на названия: ROUND_UP означает "от нуля", а не "вверх". Для округления вверх в математическом смысле нужен ROUND_CEILING.
Округление в расчетах с деньгами
С деньгами float использовать нельзя: ошибки в последних разрядах рано или поздно превратятся в расхождение на копейку в отчете. Есть два надежных подхода.
- Целые копейки. Храните суммы в
intв минимальных единицах валюты. Сложение и вычитание точные, а форматирование для вывода делается в одном месте. - Decimal. Удобен, когда есть проценты, налоги и курсы с дробными ставками. Округляйте через
quantizeв явно оговоренных точках расчета, а не после каждой операции.
Классическая задача - разделить сумму на части так, чтобы ничего не потерялось. 100 рублей на троих не делятся поровну, и простое округление каждой доли теряет копейку:
def split_amount(total_kop: int, parts: int) -> list[int]:
base, rest = divmod(total_kop, parts)
return [base + 1 if i < rest else base for i in range(parts)]
shares = split_amount(10_000, 3)
print(shares) # -> [3334, 3333, 3333]
print(sum(shares)) # -> 10000
print([f"{s // 100}.{s % 100:02d}" for s in shares])
# -> ['33.34', '33.33', '33.33']
Функция divmod дает целую часть и остаток, а остаток раздается по одной копейке первым долям. Сумма всегда сходится. Пример расчета НДС на Decimal:
price = Decimal("1999.99")
vat_rate = Decimal("0.20")
vat = (price * vat_rate / (1 + vat_rate)).quantize(CENT, rounding=ROUND_HALF_UP)
print(vat) # -> 333.33
print(price - vat) # -> 1666.66
Подробнее о типах int, float и complex рассказано в статье числа в Python 3.
Типичные ошибки при округлении
- Округление на каждом шаге. Если округлять промежуточные результаты, ошибки складываются. Считайте с полной точностью и округляйте только итог или значения, которые по правилам должны фиксироваться (например, сумма строки в чеке).
- Decimal из float.
Decimal(0.1)переносит ошибку float в точный тип. Всегда передавайте строку или целое число. - Смешивание Decimal и float. Выражение
Decimal("1.5") * 1.1выброситTypeError. Это защита: Python не дает незаметно испортить точные вычисления. Приводите все операнды кDecimal. - Округление ради сравнения.
round(a, 2) == round(b, 2)может дать разный результат для почти равных чисел по разные стороны от границы округления. Для сравнения используйтеmath.isclose(a, b, abs_tol=0.005). - Ожидание нулей в конце от round.
round(2.50, 2)печатается как 2.5. Нули в конце - задача форматирования, а не округления.
Отдельно о модуле statistics и функциях вроде sum: при сложении множества float ошибки накапливаются. Для точной суммы вещественных чисел есть math.fsum, которая отслеживает потерянные разряды: sum([0.1] * 10) дает 0.9999999999999999, а math.fsum([0.1] * 10) - ровно 1.0.
Как выбрать способ округления
- Нужно только показать число -
f"{x:.2f}", само значение не трогайте. - Нужно ближайшее целое в обычных расчетах -
round(x), помня о половинках к четному. - Нужно строго вверх или вниз до целого -
math.ceilилиmath.floor; для целочисленного деления --(-a // b). - Нужно школьное округление или точные десятичные знаки -
Decimalиз строки иquantize(..., ROUND_HALF_UP). - Деньги - целые копейки или
Decimal, никогдаfloat. - Сравнение вещественных чисел -
math.isclose, а не==.
Частые вопросы
Как в Python сделать обычное математическое округление, где 2.5 дает 3?
Используйте Decimal: Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP). Для float можно написать int(Decimal(str(x)).quantize(Decimal("1"), rounding=ROUND_HALF_UP)). Самодельный вариант math.floor(x + 0.5) работает для положительных чисел, но неверно обрабатывает отрицательные половинки.
Почему round(x) возвращает int, а round(x, 0) - float?
Так задумано: без второго аргумента результат - целое число, с аргументом сохраняется исходный тип. Если нужен int после округления до знаков, просто не передавайте второй аргумент.
Как вывести 5 как 5.00?
Через форматирование: f"{5:.2f}" или format(5, ".2f"). Функция round нули в конце не добавляет, потому что у числа нет "количества знаков", это свойство строкового представления.
Как отбросить знаки после запятой без округления?
До целого - math.trunc(x) или int(x). До двух знаков надежно - Decimal(str(x)).quantize(Decimal("0.01"), rounding=ROUND_DOWN). Вариант math.trunc(x * 100) / 100 страдает от той же ошибки умножения, что и пример с 1.1.
Округляет ли numpy так же, как round?
Да, numpy.round и numpy.around тоже используют округление половины к четному. Для школьного правила в массивах обычно применяют numpy.floor(x + 0.5) с теми же оговорками про отрицательные числа и точность float.