2.39. Lavorare con i float¶
I numeri in virgola mobile sembrano normali numeri decimali, ma alcuni dei loro comportamenti sorprendono i lettori che non li hanno mai incontrati prima – e uno di questi comportamenti è più marcato su MicroPython che sul Python desktop. Questa pagina spiega cosa aspettarsi e come scrivere codice con i float che non si comporti male in modo silenzioso.
2.39.1. Precisione¶
Il tipo float di Python è un numero in virgola mobile binario IEEE 754. Sulla maggior parte delle build di MicroPython è a precisione singola (32 bit), mentre il CPython desktop usa la precisione doppia (64 bit). La precisione singola offre circa sette cifre decimali di accuratezza; quella doppia ne offre circa quindici.
>>> 0.1 + 0.2
0.3000000
>>> 1.0 / 3.0
0.3333333
>>> 1e30 * 1e30
inf
Anche l’intervallo rappresentabile è più stretto a entrambi gli estremi: i numeri con magnitudine maggiore di circa 3.4e38 vanno in overflow a inf, e i numeri più piccoli di circa 1.2e-38 vengono arrotondati a zero.
2.39.2. Confrontare i float¶
L’insidia più comune è verificare l’uguaglianza con ==:
>>> 0.1 + 0.2 == 0.3
False
Entrambe le espressioni sembrano dover essere uguali, ma il risultato di 0.1 + 0.2 è il valore rappresentabile più vicino, che non è esattamente 0.3. Usa invece un controllo di tolleranza – chiediti se due float sono abbastanza vicini piuttosto che identici:
if abs(a - b) < 1e-6:
# close enough
...
La scelta della tolleranza dipende dalla scala dei valori. Un valore fisso 1e-6 funziona bene quando i numeri sono nell’ordine di 1; una tolleranza relativa è migliore quando i valori variano di ordini di grandezza.
math.isclose() gestisce entrambi i casi contemporaneamente:
from math import isclose
isclose(0.1 + 0.2, 0.3) # True
isclose(1.0e6 + 1, 1.0e6) # True (within default tolerance)
I due argomenti keyword controllano quale tipo di «vicinanza» conta:
rel_tol– tolleranza relativa, predefinita1e-9. Due valori corrispondono se la loro differenza rientra in questa frazione del maggiore dei due. Ottima per confronti generali su qualsiasi scala.abs_tol– tolleranza assoluta, predefinita0. Due valori corrispondono se la loro differenza rientra in questa quantità fissa.
math.isclose() restituisce True se almeno una delle tolleranze è soddisfatta. I valori predefiniti vanno bene per la maggior parte delle coppie di numeri diversi da zero; la trappola scatta quando uno dei valori può essere esattamente zero. Il controllo della tolleranza relativa equivale a «differenza ≤ rel_tol × valore più grande», e il valore più grande è zero, quindi il controllo fallisce sempre:
>>> isclose(0.0, 1e-12)
False
Il controllo della tolleranza assoluta non ha questo problema – passa un abs_tol ogni volta che lo zero è un valore con cui potresti dover confrontare:
>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True
2.39.3. Deriva di accumulo¶
Le somme lunghe di float perdono precisione più rapidamente su MicroPython che su CPython, perché ogni risultato intermedio viene riarrotondato alla precisione a 32 bit:
total = 0.0
for _ in range(1000000):
total += 0.1
print(total) # noticeably off from 100000.0
Per le addizioni ripetute in cui l’accuratezza è importante, aiutano due approcci:
Accumula in un intero ogni volta che i valori possono essere scalati a interi – lavora in millisecondi anziché in secondi, o in millivolt anziché in volt, poi converti una sola volta alla fine.
Calcola in lotti più piccoli e somma i risultati dei lotti, così ogni addizione avviene tra valori di magnitudine simile.
Il lato degli interi non ha questo limite – gli interi di MicroPython sono a precisione arbitraria, proprio come quelli di CPython. Quando puoi scegliere, preferisci l’aritmetica intera per qualsiasi cosa in cui la perdita di precisione si accumulerebbe.