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, predefinita 1e-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, predefinita 0. 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.