2.39. Trabajar con números de coma flotante¶
Los números de coma flotante parecen decimales corrientes, pero algunos de sus comportamientos sorprenden a quienes no se han topado con ellos antes, y uno de esos comportamientos es más acusado en MicroPython que en el Python de escritorio. Esta página explica qué esperar y cómo escribir código con coma flotante que no se comporte mal de forma silenciosa.
2.39.1. Precisión¶
El tipo float de Python es un número de coma flotante binario IEEE 754. En la mayoría de las compilaciones de MicroPython es de precisión simple (32 bits), mientras que CPython de escritorio usa precisión doble (64 bits). La precisión simple ofrece unos siete dígitos decimales de exactitud; la doble, unos quince.
>>> 0.1 + 0.2
0.3000000
>>> 1.0 / 3.0
0.3333333
>>> 1e30 * 1e30
inf
El rango representable también es más estrecho en ambos extremos: los números cuya magnitud supera aproximadamente 3.4e38 desbordan a inf, y los números menores que aproximadamente 1.2e-38 se redondean a cero.
2.39.2. Comparar números de coma flotante¶
El error más común es comprobar la igualdad con ==:
>>> 0.1 + 0.2 == 0.3
False
Ambas expresiones parecen que deberían ser iguales, pero el resultado de 0.1 + 0.2 es el valor representable más cercano, que no es exactamente 0.3. En su lugar, usa una comprobación de tolerancia: pregunta si dos números de coma flotante están lo bastante cerca, en vez de si son idénticos:
if abs(a - b) < 1e-6:
# close enough
...
La elección de la tolerancia depende de la escala de los valores. Un valor fijo de 1e-6 funciona bien cuando los números son del orden de 1; una tolerancia relativa es mejor cuando los valores varían en varios órdenes de magnitud.
math.isclose() gestiona ambas a la vez:
from math import isclose
isclose(0.1 + 0.2, 0.3) # True
isclose(1.0e6 + 1, 1.0e6) # True (within default tolerance)
Los dos argumentos de palabra clave controlan qué tipo de «cercanía» cuenta:
rel_tol: tolerancia relativa, valor por defecto1e-9. Dos valores coinciden si su diferencia está dentro de esta fracción del mayor de ellos. Adecuada para comparaciones generales en cualquier escala.abs_tol: tolerancia absoluta, valor por defecto0. Dos valores coinciden si su diferencia está dentro de esta cantidad fija.
math.isclose() devuelve True si se cumple cualquiera de las dos tolerancias. Los valores por defecto son adecuados para la mayoría de los pares de números distintos de cero; la trampa surge cuando uno de los valores puede ser exactamente cero. La comprobación de tolerancia relativa equivale a «diferencia ≤ rel_tol × valor mayor», y como el valor mayor es cero, la comprobación siempre falla:
>>> isclose(0.0, 1e-12)
False
La comprobación de tolerancia absoluta no tiene ese problema: pasa un abs_tol siempre que cero sea un valor con el que puedas estar comparando:
>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True
2.39.3. Deriva por acumulación¶
Las sumas largas de números de coma flotante pierden precisión más rápido en MicroPython que en CPython, porque cada resultado intermedio se vuelve a redondear a precisión de 32 bits:
total = 0.0
for _ in range(1000000):
total += 0.1
print(total) # noticeably off from 100000.0
Para sumas repetidas en las que la exactitud importa, ayudan dos patrones:
Acumular en un entero siempre que los valores puedan escalarse a enteros: trabaja en milisegundos en lugar de segundos, o en milivoltios en lugar de voltios, y convierte una sola vez al final.
Calcular en lotes más pequeños y sumar los resultados de los lotes, de modo que cada suma se haga entre valores de magnitud similar.
El lado de los enteros no tiene tal límite: los enteros de MicroPython son de precisión arbitraria, igual que los de CPython. Cuando tengas la opción, prefiere la aritmética con enteros para todo aquello en lo que la pérdida de precisión pudiera acumularse.