Представьте, что вы пишете программу для интернет-магазина и считаете сумму чека: цена товара умножается на количество, потом прибавляется налог. Всё просто — пока однажды сумма в чеке не окажется на копейку больше или меньше правильной, хотя формула верна. Причина в том, как обычные дробные числа хранятся в памяти компьютера. Разберёмся, откуда берётся эта погрешность и какими инструментами Python её обходят, когда точность важна: для денег, точных дробей и особого класса задач с комплексными числами.
Почему 0.1 + 0.2 не равно 0.3
Представьте, что вы пытаетесь записать одну треть в виде десятичной дроби: 0.3333... — цифры после запятой никогда не заканчиваются, сколько бы вы ни писали. На бумаге вы просто округляете и пишете 0.33, зная, что это не совсем точно.
Компьютер оказывается в похожей ситуации, только с числом 0.1. Внутри памяти все числа хранятся не в привычной нам десятичной системе, а в двоичной — там есть только 0 и 1. А в двоичной системе число 0.1 — это как раз такая же бесконечная, никогда не заканчивающаяся дробь, как 1/3 в десятичной. Компьютеру приходится обрезать её до фиксированного количества разрядов, и в этом месте появляется крошечная погрешность.
Официально это поведение описывает стандарт IEEE 754 — международное соглашение о том, как именно дробные числа (в Python это тип float) хранятся в памяти любого компьютера. Стандарт используют не только в Python, но и в JavaScript, Java, C и почти во всех современных языках — так что это не недоработка конкретно Python, а общее свойство двоичного хранения дробей.
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
Разберём, что здесь происходит:
print(0.1 + 0.2)— складываем два числа, которые кажутся простыми. Выведется не0.3, а0.30000000000000004: за настоящей суммой прячется небольшой "хвост" из-за округления в двоичной системе.print(0.1 + 0.2 == 0.3)— сравниваем результат сложения с числом 0.3 через==. ВыведетсяFalse, хотя на первый взгляд числа должны совпасть: слева на самом деле хранится 0.30000000000000004, а справа — чуть другое приближение числа 0.3.
Как с этим жить. Правило простое: никогда не сравнивайте два float напрямую через ==, если они получены вычислениями. Вместо этого проверяют, что числа отличаются меньше, чем на допустимую погрешность, например так: abs(a - b) < 0.0001. Здесь abs() — уже знакомая вам функция модуля числа, а условие проверяет, что разница между a и b меньше выбранной маленькой величины.
Decimal: точная арифметика для денег
Когда вы считаете деньги, погрешность в тысячные доли копейки — это не мелочь, а потенциальная ошибка в бухгалтерии. Бухгалтер, который считает на калькуляторе, никогда не получит "лишний мусор" после запятой: он оперирует ровно теми цифрами, которые записал. Именно так работают и числа типа Decimal — они хранят ровно те десятичные цифры, которые вы указали, без перевода в двоичное приближение.
Официально это называется модулем decimal, а числа в нём — объектами класса Decimal. В отличие от float, который всегда хранит число в двоичном виде, Decimal хранит его как десятичную дробь — ровно так, как оно записано на бумаге.
from decimal import Decimal
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b)
print(a + b == Decimal("0.3"))
Разберём построчно:
from decimal import Decimal— из модуляdecimalзабираем один инструмент, классDecimal. Дальше в коде его можно использовать под этим именем.a = Decimal("0.1")— создаём точное десятичное число. Обратите внимание: число передано в виде строки"0.1", а не как обычное число 0.1. Почему это важно — в следующем разделе.b = Decimal("0.2")— аналогично создаём второе число.print(a + b)— складываем два объекта Decimal тем же оператором+, что и с обычными числами. Выведется ровно0.3, без лишних цифр.print(a + b == Decimal("0.3"))— на этот раз сравнение через==сработает как ожидается и выведетTrue, потому что обе стороны хранят точные десятичные значения, а не двоичные приближения.
Пример из жизни: чек с налогом
from decimal import Decimal
price = Decimal("19.99")
tax = Decimal("0.18")
total = price * (Decimal("1") + tax)
print(total)
print(round(total, 2))
Здесь price и tax — точная цена и точная ставка налога 18%. Строка total = price * (Decimal("1") + tax) считает цену с налогом: сначала складывает единицу и ставку налога (получая множитель 1.18), затем умножает на него цену. Выведется 23.5882 — ровно столько знаков после запятой, сколько получилось математически, без случайного "мусора" в конце. Знакомая функция round(total, 2) округлит эту сумму до привычных двух знаков: 23.59.
Официальное правило: создавайте Decimal только из строки. Так, Decimal("0.1") — правильно, а Decimal(0.1) — нет.
bad = Decimal(0.1)
print(bad)
good = Decimal("0.1")
print(good)
Если создать Decimal из уже готового числа 0.1, Python сначала честно превращает его в float — со всей его двоичной погрешностью — и только потом "упаковывает" в Decimal. Строка bad = Decimal(0.1) выведет длинное число вроде 0.1000000000000000055511151231257827021181583404541015625: Decimal лишь точно скопировал ту погрешность, что уже была в float. А строка good = Decimal("0.1") создаёт число напрямую из текста, минуя float, и печатает ровно 0.1. Отсюда правило: если число заранее известно как строка или литерал в коде — пишите его в кавычках.
Fraction: числа как в учебнике математики
Вспомните, как в школе складывали дроби: 1/3 + 1/6 приводили к общему знаменателю и получали ровно 1/2, без единого десятичного знака. Decimal для такой задачи не подходит: он умеет точно хранить только конечные десятичные дроби, а 1/3 в десятичном виде — это снова бесконечное 0.333... Здесь нужен другой инструмент, который умеет работать именно с числителем и знаменателем.
Официально это модуль fractions, а его главный инструмент — класс Fraction. Fraction хранит число как пару "числитель, знаменатель" и умеет складывать, вычитать, умножать и делить такие пары так же, как вы делали это на бумаге: приводя к общему знаменателю и сокращая результат.
from fractions import Fraction
a = Fraction(1, 3)
b = Fraction(1, 6)
print(a + b)
print(a * b)
Разберём построчно:
from fractions import Fraction— забираем класс Fraction из модуля fractions, как раньше забирали Decimal из decimal.a = Fraction(1, 3)— создаём дробь 1/3, передавая числитель и знаменатель двумя отдельными аргументами.b = Fraction(1, 6)— аналогично создаём дробь 1/6.print(a + b)— складываем две дроби оператором+. Python сам находит общий знаменатель и сокращает результат: выведется1/2, а не что-то вроде6/18.print(a * b)— умножаем дроби. Выведется1/18: числители и знаменатели перемножились, как и положено при умножении дробей.
Другие способы создать Fraction
f1 = Fraction(6, 4)
print(f1)
f2 = Fraction("0.25")
print(f2)
Строка f1 = Fraction(6, 4) создаёт дробь из двух целых чисел, которые не обязаны быть уже сокращены — Fraction сокращает их сам, поделив оба числа на их наибольший общий делитель. Выведется 3/2, а не 6/4. Строка f2 = Fraction("0.25") показывает ещё один способ — создать дробь прямо из строки с десятичной записью: Python сам превращает 0.25 в дробь и сокращает её до 1/4.
Когда выбирать что. Decimal подходит для денег и любых расчётов, где число изначально задано в десятичном виде: цены, налоги, проценты. Fraction подходит там, где точнее удобнее думать дробями: пропорции, вероятности, математические задачи, где 1/3 должна остаться ровно 1/3, а не превратиться в бесконечную десятичную запись.
Коротко: комплексные числа встроены в язык
В школьной математике квадратный корень из отрицательного числа считается "не существующим". Но в некоторых областях — например, при обработке звука или в электротехнике — такие числа всё же нужны, и математики договорились считать, что корень из −1 существует, обозначив его особым числом. В Python этот тип данных встроен в язык на уровне синтаксиса, без всякого импорта:
z = 3 + 4j
print(z.real)
print(z.imag)
print(abs(z))
Здесь z = 3 + 4j создаёт комплексное число: 3 — обычная (действительная) часть, а 4j — мнимая часть, где буква j прямо в числе означает, что это особая единица. Атрибуты z.real и z.imag возвращают эти две части по отдельности (3.0 и 4.0), а уже знакомая функция abs(z) для комплексного числа считает не модуль по знаку, а длину: 5.0, по теореме Пифагора для 3 и 4. Для более сложной математики с такими числами (корень из отрицательного, тригонометрия) в стандартной библиотеке есть отдельный модуль cmath — для большинства задач этого курса он не понадобится, но полезно знать, что он существует, если вы когда-нибудь встретите обработку сигналов или похожую область.
Сводная таблица: какой тип выбрать
| Тип | Когда использовать |
|---|---|
float |
Обычные вычисления, где небольшая погрешность не критична: координаты, статистика, физические величины |
Decimal |
Деньги, налоги, проценты — всё, где важна точность до копейки, а числа заданы в десятичном виде |
Fraction |
Точные дроби и пропорции, где важно сохранить именно рациональное число, а не десятичное приближение |
complex |
Узкие задачи вроде обработки сигналов и электротехники, встречаются редко |
Частые ошибки
Сравнение float через ==. Код вида if price_total == 23.59: может неожиданно оказаться False, даже если на глаз числа одинаковые — из-за той же погрешности двоичного представления. Для float сравнивайте через допустимую разницу, а если речь о деньгах — используйте Decimal с самого начала расчёта.
Decimal, созданный из float. Decimal(19.99) выглядит правильно, но на самом деле передаёт в Decimal уже испорченное число: погрешность float никуда не денется. Всегда пишите цену строкой: Decimal("19.99").
Смешивание Decimal и float в одном выражении. Код вроде Decimal("10") + 0.5 вызовет ошибку TypeError: Python не станет молча превращать один тип в другой. Приведите оба числа к одному типу заранее, например через Decimal("0.5").
Что важно запомнить
0.1 + 0.2 != 0.3в Python — следствие стандарта IEEE 754, а не ошибка языка: дробные числа хранятся в двоичном виде с округлением.- Для денег и точных десятичных расчётов используйте
from decimal import Decimal. - Всегда создавайте Decimal из строки:
Decimal("0.1"), а не из floatDecimal(0.1). Fractionхранит число как числитель и знаменатель, складывает и умножает дроби точно, автоматически сокращая результат.- Комплексные числа встроены в Python как тип
complex:3 + 4j, без импорта. Атрибуты.real,.imagи функцияabs()— основные операции с ними. - Decimal и float нельзя складывать напрямую — сначала приведите оба числа к одному типу.
Проверьте себя
7 вопросов
Расчёт стоимости с НДС через Decimal
Напишите функцию calculate_total(price_str, tax_rate_str), которая принимает цену и ставку налога как строки и возвращает итоговую сумму с налогом, округлённую до 2 знаков после запятой.
Используйте Decimal для точных вычислений и создавайте его только из строки. Формула: итог = цена * (1 + ставка налога).
Например, calculate_total("19.99", "0.18") должна вернуть Decimal("23.59").