Learning Platform
Глоссарий Troubleshooting
Урок 02.03 · 18 мин
Средний
IEEE 754FloatBoolSubclass
Требуемые знания:
  • 02-pylong-internals

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
WARNING

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.cPyBool_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 — два разных объекта в памяти (даже если значение «то же»).

TIP

Иногда удобно использовать 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

Ключевые выводы

  1. float = C double = IEEE 754 64-bit. Approximate representation; 0.1 + 0.2 != 0.3 — это not bug, это математическое следствие двоичной системы. Для money — decimal.Decimal.
  2. math.isclose(a, b, rel_tol=...) — правильный способ сравнить float, а не ==.
  3. bool — PyLong subclass с двумя singletons Py_True/Py_False. True == 1 (значения), но True is 1 False (identity). isinstance(True, int) True.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Утверждение: «`0.1 + 0.2 == 0.3` возвращает True в стандартном Python».

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7