Числа и арифметика
Теория
В 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. Считаем стоимость позиций
Умножаем цену на количество и складываем позиции:
mouse = 1499 * 3 keyboard = 5990 * 1 print(mouse, keyboard, mouse + keyboard)
Результат:
4497 5990 10487
Шаг 2. Учитываем приоритет операций
Как в математике: сначала **, потом *, /, //, %, потом + и -. Скобки меняют порядок:
print(1499 + 5990 * 2) print((1499 + 5990) * 2)
Результат:
13479 14978
Первая строка — мышь и две клавиатуры, вторая — два комплекта «мышь + клавиатура». Если порядок неочевиден, ставьте скобки.
Шаг 3. Делим
Обычное деление / всегда возвращает float, даже если делится нацело:
print(4497 / 3) print(type(4497 / 3))
Результат:
1499.0 <class 'float'>
Если нужно целое — используйте //.
Шаг 4. Делим нацело и берём остаток
На складе 20 мышей, в коробку помещается 6. Сколько полных коробок и сколько штук останется:
items = 20 per_box = 6 print(items // per_box, items % per_box)
Результат:
3 2
// округляет вниз: для отправки понадобится не 3 коробки, а 4 — две мыши тоже нужно куда-то положить.
Шаг 5. Меняем значение на месте
Операторы +=, -=, *= меняют переменную, используя её текущее значение:
stock = 12 stock -= 1 # продали одну stock += 5 # пришла поставка print(stock)
Результат:
16
Шаг 6. Видим проблему float
Три товара по 1499,90 ₽:
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 из строки — тогда значение точное:
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 с явным правилом округления:
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».