2.39. Работа с числами с плавающей точкой

Числа с плавающей точкой выглядят как обычные десятичные дроби, но некоторые их особенности удивляют тех, кто раньше с ними не сталкивался – и одна из этих особенностей выражена на MicroPython сильнее, чем на настольном Python. На этой странице рассказано, чего ожидать и как писать код с плавающей точкой, который не будет молча работать неправильно.

2.39.1. Точность

Тип float в Python – это число с плавающей точкой по стандарту IEEE 754. В большинстве сборок MicroPython оно имеет одинарную точность (32 бита), тогда как настольный CPython использует двойную точность (64 бита). Одинарная точность даёт около семи десятичных знаков точности; двойная – около пятнадцати.

>>> 0.1 + 0.2
0.3000000

>>> 1.0 / 3.0
0.3333333

>>> 1e30 * 1e30
inf

Представимый диапазон также уже с обоих концов: числа, превышающие по модулю примерно 3.4e38, переполняются до inf, а числа меньше примерно 1.2e-38 округляются до нуля.

2.39.2. Сравнение чисел с плавающей точкой

Самая распространённая ловушка – проверка на равенство с помощью ==:

>>> 0.1 + 0.2 == 0.3
False

Оба выражения выглядят так, будто должны быть равны, но результат 0.1 + 0.2 – это ближайшее представимое значение, которое не равно в точности 0.3. Вместо этого используйте проверку с допуском – спрашивайте, достаточно ли два числа близки, а не идентичны ли они:

if abs(a - b) < 1e-6:
    # close enough
    ...

Выбор допуска зависит от масштаба значений. Фиксированный 1e-6 хорошо работает, когда числа имеют порядок около 1; относительный допуск лучше, когда значения различаются на порядки.

math.isclose() обрабатывает оба случая сразу:

from math import isclose

isclose(0.1 + 0.2, 0.3)         # True
isclose(1.0e6 + 1, 1.0e6)       # True (within default tolerance)

Два именованных аргумента управляют тем, какой вид «близости» учитывается:

  • rel_tolотносительный допуск, по умолчанию 1e-9. Два значения совпадают, если их разность находится в пределах этой доли от большего из них. Подходит для общих сравнений в любом масштабе.

  • abs_tolабсолютный допуск, по умолчанию 0. Два значения совпадают, если их разность находится в пределах этой фиксированной величины.

math.isclose() возвращает True, если выполнен хотя бы один из допусков. Значения по умолчанию подходят для большинства пар ненулевых чисел; ловушка возникает, когда одно из значений может быть в точности нулём. Проверка относительного допуска сводится к «разность ≤ rel_tol × наибольшее значение», а наибольшее значение равно нулю, поэтому проверка всегда не проходит:

>>> isclose(0.0, 1e-12)
False

У проверки абсолютного допуска такой проблемы нет – передавайте abs_tol всякий раз, когда ноль является значением, с которым вы можете сравнивать:

>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True

2.39.3. Накопление погрешности

Длинные суммы чисел с плавающей точкой теряют точность на MicroPython быстрее, чем на CPython, потому что каждый промежуточный результат округляется обратно до 32-битной точности:

total = 0.0
for _ in range(1000000):
    total += 0.1

print(total)        # noticeably off from 100000.0

Для повторяющихся сложений, где важна точность, помогают два приёма:

  • Накапливайте в целое число всякий раз, когда значения можно масштабировать до целых – работайте в миллисекундах вместо секунд или в милливольтах вместо вольт, а затем выполните единственное преобразование в конце.

  • Вычисляйте меньшими партиями и суммируйте результаты партий, чтобы каждое сложение происходило между значениями схожего порядка.

У целочисленной стороны такого ограничения нет – целые числа MicroPython имеют произвольную точность, как и в CPython. Там, где есть выбор, предпочитайте целочисленную арифметику для всего, где потеря точности накапливалась бы.