Числовые типы: int, float, Decimal, complex

В большинстве языков int имеет фиксированную ширину - 32 или 64 бита, и может переполниться. float оперирует приближением, и 0.1 + 0.2 != 0.3. В Python обе вещи устроены иначе - целые произвольной точности, а для денежных расчётов есть Decimal. В этом уроке разберём четыре числовых типа и когда какой выбирать.

int - целое произвольной точности

Целые в Python не переполняются. Размер ограничен только доступной памятью:

>>> 2 ** 1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

Это не магия - под капотом int хранится как массив машинных слов, и операции работают через арифметику длинных чисел. Это медленнее фиксированного int, но никогда не «переедет» в отрицательное значение из-за overflow.

В Go `int` обычно 64-битный и переполняется, а [преобразования типов там явные](../go/19-type-conversion.md). В C `int` зависит от платформы. В JavaScript - тоже плавающая точка под капотом для всех чисел до BigInt. Python подходит ближе к «математическому» поведению.

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')
Никогда не считай деньги в `float`. Накапливающиеся погрешности приводят к недостачам или излишкам в копейках. В банковском backend всегда `Decimal` или целое количество центов/копеек (например, `1050` для $10.50).

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                   - остаток всегда того же знака что делитель
В Python `//` это **floor division** - округление к минус бесконечности, не к нулю. Это даёт инвариант `(a // b) * b + a % b == a` для любых знаков. В C и Go `/` для целых округляет к нулю, и поведение остатка может отличаться.

Приведения типов:

>>> 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

Мини-задание

  1. Проверь что int не переполняется:
n = 2 ** 100
print(n)
print(type(n))
  1. Сравни 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))
  1. Реализуй денежный расчёт через 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}")
  1. Попробуй банковское округление:
print(round(0.5))   # 0
print(round(1.5))   # 2
print(round(2.5))   # 2
print(round(3.5))   # 4

Объясни себе результат.

Что дальше

Разобрались с числами. В следующем уроке - строки и байты: чем str отличается от bytes, как работает кодировка, и зачем нужны f-strings.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.