2.39. Lucrul cu numere în virgulă mobilă¶
Numerele în virgulă mobilă arată ca niște zecimale obișnuite, dar câteva dintre comportamentele lor îi surprind pe cei care nu le-au mai întâlnit – iar unul dintre aceste comportamente este mai pronunțat pe MicroPython decât pe Python de desktop. Această pagină acoperă la ce să te aștepți și cum să scrii cod cu virgulă mobilă care nu se comportă greșit pe ascuns.
2.39.1. Precizie¶
Tipul float din Python este un număr în virgulă mobilă binar IEEE 754. Pe majoritatea versiunilor MicroPython acesta are precizie simplă (32 de biți), în timp ce CPython de desktop folosește precizie dublă (64 de biți). Precizia simplă oferă aproximativ șapte cifre zecimale de acuratețe; cea dublă oferă aproximativ cincisprezece.
>>> 0.1 + 0.2
0.3000000
>>> 1.0 / 3.0
0.3333333
>>> 1e30 * 1e30
inf
Intervalul reprezentabil este, de asemenea, mai îngust la ambele capete: numerele cu o magnitudine mai mare de aproximativ 3.4e38 depășesc spre inf, iar numerele mai mici de aproximativ 1.2e-38 se rotunjesc la zero.
2.39.2. Compararea numerelor în virgulă mobilă¶
Cea mai comună capcană este testarea egalității cu ==:
>>> 0.1 + 0.2 == 0.3
False
Ambele expresii par că ar trebui să fie egale, dar rezultatul lui 0.1 + 0.2 este cea mai apropiată valoare reprezentabilă, care nu este exact 0.3. Folosește în schimb o verificare cu toleranță – întreabă dacă două numere în virgulă mobilă sunt suficient de apropiate, în loc de identice:
if abs(a - b) < 1e-6:
# close enough
...
Alegerea toleranței depinde de scara valorilor. O valoare fixă 1e-6 funcționează bine când numerele sunt în jurul ordinului 1; o toleranță relativă este mai bună când valorile variază cu mai multe ordine de mărime.
math.isclose() le tratează pe ambele deodată:
from math import isclose
isclose(0.1 + 0.2, 0.3) # True
isclose(1.0e6 + 1, 1.0e6) # True (within default tolerance)
Cele două argumente-cheie controlează ce fel de „apropiere” contează:
rel_tol– toleranță relativă, implicit1e-9. Două valori se potrivesc dacă diferența lor se încadrează în această fracțiune din cea mai mare dintre ele. Bună pentru comparații generale la orice scară.abs_tol– toleranță absolută, implicit0. Două valori se potrivesc dacă diferența lor se încadrează în această cantitate fixă.
math.isclose() returnează True dacă oricare dintre toleranțe este îndeplinită. Valorile implicite sunt potrivite pentru majoritatea perechilor de numere nenule; capcana apare când una dintre valori poate fi exact zero. Verificarea cu toleranță relativă se reduce la „diferență ≤ rel_tol × cea mai mare valoare”, iar cea mai mare valoare este zero, deci verificarea eșuează întotdeauna:
>>> isclose(0.0, 1e-12)
False
Verificarea cu toleranță absolută nu are această problemă – transmite un abs_tol ori de câte ori zero este o valoare cu care ai putea face comparații:
>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True
2.39.3. Acumularea erorilor¶
Sumele lungi de numere în virgulă mobilă pierd precizie mai repede pe MicroPython decât pe CPython, deoarece fiecare rezultat intermediar este rotunjit înapoi la precizia de 32 de biți:
total = 0.0
for _ in range(1000000):
total += 0.1
print(total) # noticeably off from 100000.0
Pentru adunări repetate unde acuratețea contează, ajută două tipare:
Acumulează într-un întreg ori de câte ori valorile pot fi scalate la întregi – lucrează în milisecunde în loc de secunde, sau în milivolți în loc de volți, apoi convertește o singură dată la final.
Calculează în loturi mai mici și însumează rezultatele loturilor, astfel încât fiecare adunare să se facă între valori de magnitudine similară.
Partea cu numere întregi nu are o astfel de limită – numerele întregi din MicroPython au precizie arbitrară, la fel ca cele din CPython. Acolo unde ai posibilitatea, preferă aritmetica cu numere întregi pentru orice situație în care pierderea de precizie s-ar acumula.