2.39. Werken met floats¶
Drijvendekommagetallen zien eruit als gewone decimalen, maar enkele van hun gedragingen verrassen lezers die er nog niet eerder mee te maken hebben gehad – en een van die gedragingen is op MicroPython sterker uitgesproken dan op desktop-Python. Deze pagina behandelt wat je kunt verwachten en hoe je float-code schrijft die niet stilletjes verkeerd gaat werken.
2.39.1. Precisie¶
Pythons float is een IEEE 754 binair drijvendekommagetal. Op de meeste MicroPython-builds is het single precision (32-bit), terwijl desktop-CPython double precision (64-bit) gebruikt. Single precision draagt ongeveer zeven decimale cijfers nauwkeurigheid; double draagt er ongeveer vijftien.
>>> 0.1 + 0.2
0.3000000
>>> 1.0 / 3.0
0.3333333
>>> 1e30 * 1e30
inf
Het representeerbare bereik is aan beide uiteinden ook smaller: getallen groter dan ongeveer 3.4e38 in absolute waarde lopen over naar inf, en getallen kleiner dan ongeveer 1.2e-38 worden afgerond naar nul.
2.39.2. Floats vergelijken¶
De meest voorkomende valkuil is het testen op gelijkheid met ==:
>>> 0.1 + 0.2 == 0.3
False
Beide uitdrukkingen zien eruit alsof ze gelijk zouden moeten zijn, maar het resultaat van 0.1 + 0.2 is de dichtstbijzijnde representeerbare waarde, die niet exact 0.3 is. Gebruik in plaats daarvan een tolerantiecontrole – vraag of twee floats dicht genoeg bij elkaar liggen in plaats van identiek te zijn:
if abs(a - b) < 1e-6:
# close enough
...
De keuze van de tolerantie hangt af van de schaal van de waarden. Een vaste 1e-6 werkt goed wanneer de getallen rond de orde van 1 liggen; een relatieve tolerantie is beter wanneer de waarden over ordes van grootte variëren.
math.isclose() handelt beide tegelijk af:
from math import isclose
isclose(0.1 + 0.2, 0.3) # True
isclose(1.0e6 + 1, 1.0e6) # True (within default tolerance)
De twee keyword-argumenten bepalen welk soort “dichtbij” telt:
rel_tol– relatieve tolerantie, standaard1e-9. Twee waarden komen overeen als hun verschil binnen deze fractie van de grootste van de twee ligt. Goed voor algemene vergelijkingen op elke schaal.abs_tol– absolute tolerantie, standaard0. Twee waarden komen overeen als hun verschil binnen dit vaste bedrag ligt.
math.isclose() geeft True terug als aan één van beide toleranties wordt voldaan. De standaardwaarden zijn prima voor de meeste paren van niet-nul getallen; de valkuil is wanneer een van de waarden exact nul kan zijn. De relatieve-tolerantiecontrole komt neer op “verschil ≤ rel_tol × grootste waarde”, en de grootste waarde is nul, dus de controle faalt altijd:
>>> isclose(0.0, 1e-12)
False
De absolute-tolerantiecontrole heeft dat probleem niet – geef een abs_tol mee telkens wanneer nul een waarde is waarmee je mogelijk vergelijkt:
>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True
2.39.3. Accumulatiedrift¶
Lange sommen van floats verliezen op MicroPython sneller precisie dan op CPython, omdat elk tussenresultaat weer wordt afgerond naar 32-bit precisie:
total = 0.0
for _ in range(1000000):
total += 0.1
print(total) # noticeably off from 100000.0
Voor herhaalde optellingen waar nauwkeurigheid van belang is, helpen twee patronen:
Accumuleer in een integer telkens wanneer de waarden naar gehele getallen geschaald kunnen worden – werk in milliseconden in plaats van seconden, of in millivolt in plaats van volt, en converteer dan één keer aan het eind.
Reken in kleinere batches en tel de batchresultaten op, zodat elke optelling tussen waarden van vergelijkbare grootte plaatsvindt.
De integer-kant heeft zo’n limiet niet – MicroPython-integers hebben arbitraire precisie, net als die van CPython. Waar je de keuze hebt, geef de voorkeur aan integer-rekenkunde voor alles waar het precisieverlies zich zou opstapelen.