Числа и арифметика

Теория

В Python два основных числовых типа: int — целые числа и float — дробные. int не ограничен по размеру: Python посчитает и 2 ** 100. float хранит число в двоичном виде с ограниченной точностью, поэтому многие десятичные дроби (0.1, 19.9) в нём записаны приблизительно — отсюда 0.1 + 0.2 = 0.30000000000000004.

Для backend это важно из-за денег: копейки, потерянные в float, превращаются в расхождения в отчётах и чеках. Поэтому деньги считают в Decimal из стандартного модуля decimal — он хранит десятичные дроби точно и умеет округлять по правилам бухгалтерии. В этой теме — арифметические операторы, их приоритет, деление нацело и остаток, округление и работа с Decimal.

Наглядно

Таблица 1. Арифметические операторы Python на примерах из магазина.

Оператор Что делает Пример Результат
+ - * сложение, вычитание, умножение 1499 * 3 4497
/ деление — всегда даёт float 4497 / 3 1499.0
// деление нацело (округляет вниз) 20 // 6 — полных коробок 3
% остаток от деления 20 % 6 — товаров вне коробок 2
** степень 2 ** 10 1024
+= -= *= изменить переменную на месте stock -= 1 то же, что stock = stock - 1

Таблица 2. Какой числовой тип для чего брать в магазине.

Тип Точность Где использовать Пример
int точный количество, остаток, id, цена в копейках stock = 12
float приблизительный рейтинг, вес, проценты в аналитике rating = 4.7
Decimal точный, десятичный цены, суммы, скидки — все деньги Decimal("1499.90")

На практике

Задача: посчитать чек — стоимость позиций, упаковку по коробкам, скидку — так, чтобы сумма сходилась до копейки.

Шаг 1. Считаем стоимость позиций

Умножаем цену на количество и складываем позиции:

python
mouse = 1499 * 3
keyboard = 5990 * 1
print(mouse, keyboard, mouse + keyboard)

Результат:

вывод
4497 5990 10487

Шаг 2. Учитываем приоритет операций

Как в математике: сначала **, потом *, /, //, %, потом + и -. Скобки меняют порядок:

python
print(1499 + 5990 * 2)
print((1499 + 5990) * 2)

Результат:

вывод
13479
14978

Первая строка — мышь и две клавиатуры, вторая — два комплекта «мышь + клавиатура». Если порядок неочевиден, ставьте скобки.

Шаг 3. Делим

Обычное деление / всегда возвращает float, даже если делится нацело:

python
print(4497 / 3)
print(type(4497 / 3))

Результат:

вывод
1499.0
<class 'float'>

Если нужно целое — используйте //.

Шаг 4. Делим нацело и берём остаток

На складе 20 мышей, в коробку помещается 6. Сколько полных коробок и сколько штук останется:

python
items = 20
per_box = 6
print(items // per_box, items % per_box)

Результат:

вывод
3 2

// округляет вниз: для отправки понадобится не 3 коробки, а 4 — две мыши тоже нужно куда-то положить.

Шаг 5. Меняем значение на месте

Операторы +=, -=, *= меняют переменную, используя её текущее значение:

python
stock = 12
stock -= 1      # продали одну
stock += 5      # пришла поставка
print(stock)

Результат:

вывод
16

Шаг 6. Видим проблему float

Три товара по 1499,90 ₽:

python
print(1499.9 * 3)
print(0.1 + 0.2 == 0.3)

Результат:

вывод
4499.700000000001
False

1499.9 и 0.1 в двоичном виде не записываются точно — получаются «лишние» копейки, а сравнение через == ломается. Для денег это недопустимо.

Шаг 7. Считаем деньги в Decimal

Decimal живёт в модуле decimal стандартной библиотеки — строка from decimal import Decimal подключает его в программу (как устроены импорты — в разделе «Модули»). Создаём Decimal из строки — тогда значение точное:

python
from decimal import Decimal

price = Decimal("1499.90")
total = price * 3
print(total)
print(Decimal(1499.9))

Результат:

вывод
4499.70
1499.90000000000009094947017729282379150390625

Вторая строка — что будет, если создать Decimal из float: неточность float переедет в Decimal. Поэтому только строка или int.

Шаг 8. Округляем

Встроенный round округляет банковским способом: .5 — к ближайшему чётному. Для денег используют quantize с явным правилом округления:

python
from decimal import Decimal, ROUND_HALF_UP

print(round(2.5), round(3.5))

discounted = Decimal("999") * Decimal("0.875")   # скидка 12,5 %
print(discounted)
print(discounted.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))

Результат:

вывод
2 4
874.125
874.13

quantize(Decimal("0.01")) — «оставить два знака после запятой», ROUND_HALF_UP — привычное «пять и больше — вверх».

Частые ошибки

  • / вместо //. / всегда даёт float: 4497 / 3 — 1499.0, а не 1499. Для целых — //.
  • // там, где нужно «округлить вверх». 20 // 6 = 3, а коробок нужно 4. Округление вверх для целых: (items + per_box - 1) // per_box.
  • float для денег. 1499.9 * 3 = 4499.700000000001. Деньги — Decimal или int в копейках.
  • Decimal(19.9) вместо Decimal("19.9"). Из float в Decimal переезжает неточность. Создаём из строки.
  • round для денег. round(2.5) == 2: банковское округление. А quantize по умолчанию тоже округляет к чётному — правило ROUND_HALF_UP указываем явно.
  • Сравнение float через ==. 0.1 + 0.2 == 0.3 — False. Если float неизбежен — сравнивают с допуском (math.isclose).

Итог

Типы

  • int — целые, без ограничения размера; float — дробные, приблизительные.
  • Деньги — Decimal("…") из строки (или int в копейках), никогда не float.

Операторы

  • + - * / // % **; приоритет как в математике, скобки — когда неочевидно.
  • / всегда даёт float; // — деление нацело вниз; % — остаток.
  • x += 1 — то же, что x = x + 1.

Округление

  • round — банковское: round(2.5) == 2.
  • Деньги: value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP).
  • Округление вверх для целых: (a + b - 1) // b.

Вопросы на собеседовании

Почему 0.1 + 0.2 != 0.3?

float хранится в двоичном формате IEEE 754, а 0.1 и 0.2 не имеют точного двоичного представления — хранятся ближайшие значения. Ошибки округления складываются, и сумма получается 0.30000000000000004. Это свойство формата, а не баг Python.

Как хранить деньги в Python и в БД?

В Python — Decimal, созданный из строки, или int в минимальных единицах (копейках). В БД — numeric(10, 2) (в Django — DecimalField). float для денег не используют из-за неточности.

Что вернёт round(2.5) и почему?

2. Python использует банковское округление: половина округляется к ближайшему чётному. Так при массовом округлении ошибки не накапливаются в одну сторону. Для «школьного» округления — Decimal.quantize с ROUND_HALF_UP.

Чем / отличается от //?

/ — истинное деление, результат всегда float. // — деление с округлением вниз (к минус бесконечности): 7 // 2 == 3, -7 // 2 == -4. Для int результат // — int.

Глубже

Почему int не переполняется, как устроен float по стандарту IEEE 754 и как работает контекст точности Decimal — в курсе «Python под капотом», темы «Целые числа» и «float и Decimal».

Ошибка в тексте

Сообщить об ошибке могут зарегистрированные пользователи — так в сообщениях меньше спама.

Войти, чтобы сообщить