float (IEEE 754) и bool как PyLong subclass
Python float — это просто обёртка над C double (IEEE 754 64-bit). А bool — это наследник int (формально PyLong), у которого ровно два singleton-объекта: Py_True и Py_False. Из этих двух фактов вытекает много неинтуитивных эффектов: 0.1 + 0.2 != 0.3, True == 1 (но True is 1 — False), isinstance(True, int) — True, sum([True, False, True]) — 2.
Float — IEEE 754 double
PyFloat (см. Objects/floatobject.c) — это PyObject_HEAD плюс единственное поле ob_fval типа C double (8 байт). IEEE 754 double-precision раскладывает эти 64 бита так:
- 1 бит знака (sign).
- 11 бит экспоненты (exponent, biased на 1023).
- 52 бита мантиссы (significand, плюс implicit leading 1 для нормализованных чисел).
Это даёт ~15–17 значащих десятичных цифр, диапазон ±[2.2e-308 .. 1.8e308] для нормализованных значений и специальные битовые паттерны для inf, -inf, NaN.
sys.float_info экспортирует все эти константы:
import sys
print(sys.float_info.max) # 1.7976931348623157e+308
print(sys.float_info.min) # 2.2250738585072014e-308 (min positive normal)
print(sys.float_info.epsilon) # 2.220446049250313e-16
print(sys.float_info.mant_dig) # 53 (52 stored + 1 implicit)
Round-off: 0.1 + 0.2 != 0.3
Десятичная дробь 0.1 не имеет точного представления в двоичной системе — она бесконечно повторяется как 0.0001100110011.... IEEE 754 хранит ближайшее округление в 53 бит мантиссы, поэтому фактическое значение чуть больше 0.1. То же для 0.2. Сумма этих округлений даёт результат, отличающийся от истинного 0.3:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
Floating point — это approximate representation. Для финансовых расчётов (деньги, налоги, проценты) используйте decimal.Decimal. Для рациональных чисел — fractions.Fraction. Никогда не сравнивайте float через ==.
Machine epsilon и сравнение float
sys.float_info.epsilon ≈ 2.22e-16 — это machine epsilon: минимальное ε такое, что 1.0 + ε > 1.0 в float-арифметике. Любое меньшее значение «теряется» при сложении с 1.
Для корректного сравнения двух float используйте math.isclose, который проверяет относительную и абсолютную погрешность одновременно:
import math
a = 0.1 + 0.2
b = 0.3
print(math.isclose(a, b, rel_tol=1e-9)) # True
# ручной аналог
print(abs(a - b) < 1e-9 * max(abs(a), abs(b))) # True
rel_tol — относительный допуск (доля от max(|a|, |b|)); abs_tol — абсолютный (для значений рядом с нулём, где relative tolerance выродится в 0).
decimal и fractions для exact arithmetic
from decimal import Decimal
from fractions import Fraction
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # True
print(Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10)) # True
Decimal поддерживает произвольную десятичную точность с настраиваемым контекстом. Fraction хранит числитель и знаменатель как int (PyLong), поэтому никаких округлений нет вообще. Цена обоих — медленнее float в десятки раз.
bool как PyLong subclass
bool объявлен как наследник int в Objects/boolobject.c — PyBool_Type указывает на PyLong_Type как на base. Существует ровно два singleton-объекта: Py_True (значение 1) и Py_False (значение 0). Все ссылки на True в Python-коде указывают на один и тот же Py_True.
Из этого следует:
print(isinstance(True, int)) # True — bool is-a PyLong
print(True == 1) # True — value equality (bool наследует __eq__)
print(True is 1) # False — identity: Py_True != PyLong(1)
print(True + True) # 2 — bool наследует __add__ от PyLong
print(sum([True, False, True])) # 2 — sum работает на bool
print(type(True)) # <class 'bool'>
print(type(True).__mro__) # (<class 'bool'>, <class 'int'>, <class 'object'>)
Поведение True == 1 — следствие того, что bool.__eq__ делегируется в int.__eq__. Поведение True is 1 — два разных объекта в памяти (даже если значение «то же»).
Иногда удобно использовать sum([predicate(x) for x in items]) как «count где predicate=True». Это работает за счёт bool-as-int, но злоупотреблять не стоит — sum(1 for x in items if predicate(x)) или sum(map(predicate, items)) читаемее. И всегда явно: count = sum(...), а не загадочное total = sum(flags).
Cross-course context
Выбор числовых типов в ClickHouse: Float vs DecimalКлючевые выводы
float= Cdouble= IEEE 754 64-bit. Approximate representation;0.1 + 0.2 != 0.3— это not bug, это математическое следствие двоичной системы. Для money —decimal.Decimal.math.isclose(a, b, rel_tol=...)— правильный способ сравнить float, а не==.bool— PyLong subclass с двумя singletonsPy_True/Py_False.True == 1(значения), ноTrue is 1False (identity).isinstance(True, int)True.