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. Там, где есть выбор, предпочитайте целочисленную арифметику для всего, где потеря точности накапливалась бы.