Числовые типы: int, float, Decimal, complex
В большинстве языков int имеет фиксированную ширину - 32 или 64 бита, и может переполниться. float оперирует приближением, и 0.1 + 0.2 != 0.3. В Python обе вещи устроены иначе - целые произвольной точности, а для денежных расчётов есть Decimal. В этом уроке разберём четыре числовых типа и когда какой выбирать.
int - целое произвольной точности
Целые в Python не переполняются. Размер ограничен только доступной памятью:
>>> 2 ** 1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
Это не магия - под капотом int хранится как массив машинных слов, и операции работают через арифметику длинных чисел. Это медленнее фиксированного int, но никогда не «переедет» в отрицательное значение из-за overflow.
float - 64-битная плавающая точка
float в Python - это IEEE 754 double precision (64 бита):
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Это не баг Python - так работает float везде, где используется IEEE 754. Числа 0.1, 0.2, 0.3 нельзя точно представить в двоичной плавающей точке, поэтому при сложении возникает погрешность.
Правило: никогда не сравнивай float через ==. Используй приближённое равенство:
import math
a = 0.1 + 0.2
b = 0.3
# Плохо
print(a == b) # False
# Хорошо
print(math.isclose(a, b)) # True
print(abs(a - b) < 1e-9) # True
math.isclose(a, b, rel_tol=1e-9, abs_tol=0) - стандартный способ сравнения с настраиваемой точностью.
Decimal - точная десятичная арифметика
Для денежных расчётов и других случаев, где нужна точная десятичная арифметика, в стандартной библиотеке есть decimal.Decimal:
from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2")
Decimal('0.3')
>>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
True
Важно: создавай Decimal из строки, не из float:
# Плохо: float уже искажён, и Decimal сохранит погрешность
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')
# Хорошо
Decimal("0.1") # Decimal('0.1')
Decimal медленнее float (это арифметика на строках под капотом), но для финансовых задач это правильный выбор.
complex - комплексные числа
Python поддерживает комплексные числа из коробки:
>>> z = 2 + 3j
>>> z.real
2.0
>>> z.imag
3.0
>>> abs(z) # модуль |z|
3.605551275463989
>>> z * (1 + 1j)
(-1+5j)
Для backend-разработки complex нужен редко - в основном для научных вычислений и обработки сигналов. Но знать что такой тип есть полезно: он даёт правильное поведение при делении на ноль для отрицательных степеней (а не выбрасывает исключение как float).
Операции и приведения
Базовые операторы:
>>> 7 + 3 # 10
>>> 7 - 3 # 4
>>> 7 * 3 # 21
>>> 7 / 3 # 2.3333333333333335 - всегда float
>>> 7 // 3 # 2 - целочисленное деление
>>> 7 % 3 # 1 - остаток
>>> 7 ** 3 # 343 - степень
>>> divmod(7, 3) # (2, 1) - частное и остаток вместе
>>> -7 // 3 # -3 - округление к -∞, не к 0
>>> -7 % 3 # 2 - остаток всегда того же знака что делитель
Приведения типов:
>>> int(3.7) # 3 - усечение к нулю, не округление
>>> int("42") # 42
>>> int("0x1F", 16) # 31 - с явным основанием
>>> float("3.14") # 3.14
>>> round(3.5) # 4 (banker's rounding, см. ниже)
>>> round(2.5) # 2 - округление к ближайшему чётному
Функция round() использует banker's rounding (round half to even) - 0.5 округляется к ближайшему чётному. Это делается для уменьшения статистической погрешности при многократных округлениях. Если нужно классическое округление 0.5 → 1, используй math.ceil(x + 0.5).
Числовые литералы
Python понимает разные системы счисления:
>>> 0b1010 # бинарное: 10
>>> 0o12 # восьмеричное: 10
>>> 0xA # шестнадцатеричное: 10
>>> 10_000_000 # подчёркивания как разделитель для читаемости
>>> 1.5e3 # экспоненциальная форма: 1500.0
>>> 1.5e-3 # 0.0015
>>> 1j # мнимая единица: 0+1j
Подчёркивания в литералах (Python 3.6+) сильно облегчают чтение больших чисел: 1_000_000_000 гораздо понятнее чем 1000000000.
math и numbers
В стандартной библиотеке есть math:
import math
math.pi # 3.141592653589793
math.e # 2.718281828459045
math.inf # бесконечность
math.nan # not a number
math.floor(3.7) # 3
math.ceil(3.2) # 4
math.sqrt(16) # 4.0
math.log(100, 10) # 2.0
math.gcd(12, 18) # 6 - НОД
math.isclose(0.1+0.2, 0.3) # True
math.isnan(float("nan")) # True
Функция math.isnan() пригодится там, где нельзя сравнивать через == - подробнее в уроке про truthy/falsy и сравнения. Для теоретических обобщений есть модуль numbers с иерархией типов (Number, Real, Rational, Integral) - используется редко, но если делаешь generic-функцию для любых чисел, можно проверять isinstance(x, numbers.Real).
Проверка типов
>>> isinstance(5, int) # True
>>> isinstance(5.0, float) # True
>>> isinstance(5, (int, float)) # True - можно кортеж
>>> isinstance(True, int) # True (сюрприз!)
Сюрприз: True и False это подкласс int (1 и 0). Поэтому isinstance(True, int) возвращает True. Что из этого следует для условий - в уроке про bool, None и truthy/falsy. Если хочется отличить - сравнивай явно через type():
>>> type(True) is int # False
>>> type(1) is int # True
Мини-задание
- Проверь что
intне переполняется:
n = 2 ** 100
print(n)
print(type(n))
- Сравни 0.1 + 0.2 с 0.3:
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
import math
print(math.isclose(0.1 + 0.2, 0.3))
- Реализуй денежный расчёт через Decimal:
from decimal import Decimal
prices = [Decimal("9.99"), Decimal("4.50"), Decimal("0.99")]
total = sum(prices)
discount = Decimal("0.15")
final = total * (1 - discount)
print(f"Сумма: {total}, скидка 15%: {final}")
- Попробуй банковское округление:
print(round(0.5)) # 0
print(round(1.5)) # 2
print(round(2.5)) # 2
print(round(3.5)) # 4
Объясни себе результат.
Что дальше
Разобрались с числами. В следующем уроке - строки и байты: чем str отличается от bytes, как работает кодировка, и зачем нужны f-strings.