2.39. Arbeiten mit Floats

Gleitkommazahlen sehen aus wie gewöhnliche Dezimalzahlen, aber einige ihrer Verhaltensweisen überraschen Leser, die ihnen zuvor noch nicht begegnet sind – und eine dieser Verhaltensweisen ist auf MicroPython ausgeprägter als auf Desktop-Python. Diese Seite behandelt, was zu erwarten ist und wie man Float-Code schreibt, der sich nicht stillschweigend fehlverhält.

2.39.1. Genauigkeit

Pythons float ist eine binäre IEEE-754-Gleitkommazahl. Auf den meisten MicroPython-Builds ist sie einfach genau (32 Bit), während Desktop-CPython doppelte Genauigkeit (64 Bit) verwendet. Einfache Genauigkeit trägt etwa sieben Dezimalstellen Genauigkeit; doppelte trägt etwa fünfzehn.

>>> 0.1 + 0.2
0.3000000

>>> 1.0 / 3.0
0.3333333

>>> 1e30 * 1e30
inf

Der darstellbare Bereich ist an beiden Enden ebenfalls schmaler: Zahlen, deren Betrag größer als etwa 3.4e38 ist, laufen zu inf über, und Zahlen kleiner als etwa 1.2e-38 werden auf null gerundet.

2.39.2. Floats vergleichen

Die häufigste Falle ist das Testen auf Gleichheit mit ==:

>>> 0.1 + 0.2 == 0.3
False

Beide Ausdrücke sehen so aus, als sollten sie gleich sein, doch das Ergebnis von 0.1 + 0.2 ist der nächstgelegene darstellbare Wert, der nicht genau 0.3 ist. Verwenden Sie stattdessen eine Toleranzprüfung – fragen Sie, ob zwei Floats nahe genug beieinander liegen, statt ob sie identisch sind:

if abs(a - b) < 1e-6:
    # close enough
    ...

Die Wahl der Toleranz hängt von der Größenordnung der Werte ab. Ein festes 1e-6 funktioniert gut, wenn die Zahlen in der Größenordnung von 1 liegen; eine relative Toleranz ist besser, wenn die Werte um Größenordnungen variieren.

math.isclose() behandelt beides auf einmal:

from math import isclose

isclose(0.1 + 0.2, 0.3)         # True
isclose(1.0e6 + 1, 1.0e6)       # True (within default tolerance)

Die beiden Schlüsselwortargumente steuern, welche Art von „nah“ zählt:

  • rel_tolrelative Toleranz, Standardwert 1e-9. Zwei Werte stimmen überein, wenn ihre Differenz innerhalb dieses Anteils des größeren liegt. Gut für allgemeine Vergleiche über jede Größenordnung hinweg.

  • abs_tolabsolute Toleranz, Standardwert 0. Zwei Werte stimmen überein, wenn ihre Differenz innerhalb dieses festen Betrags liegt.

math.isclose() gibt True zurück, wenn eine der beiden Toleranzen erfüllt ist. Die Standardwerte sind für die meisten Paare von Zahlen ungleich null in Ordnung; die Falle ist, wenn einer der Werte genau null sein kann. Die relative Toleranzprüfung läuft auf „Differenz ≤ rel_tol × größter Wert“ hinaus, und der größte Wert ist null, sodass die Prüfung immer fehlschlägt:

>>> isclose(0.0, 1e-12)
False

Die absolute Toleranzprüfung hat dieses Problem nicht – übergeben Sie ein abs_tol, wann immer null ein Wert ist, gegen den Sie möglicherweise vergleichen:

>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True

2.39.3. Akkumulationsdrift

Lange Summen von Floats verlieren auf MicroPython schneller an Genauigkeit als auf CPython, weil jedes Zwischenergebnis wieder auf 32-Bit-Genauigkeit gerundet wird:

total = 0.0
for _ in range(1000000):
    total += 0.1

print(total)        # noticeably off from 100000.0

Für wiederholte Additionen, bei denen Genauigkeit wichtig ist, helfen zwei Muster:

  • Akkumulieren Sie in eine Ganzzahl, wann immer die Werte auf Ganzzahlen skaliert werden können – rechnen Sie in Millisekunden statt Sekunden oder in Millivolt statt Volt und konvertieren Sie erst am Ende einmal.

  • Rechnen Sie in kleineren Stapeln und summieren Sie die Stapelergebnisse, sodass jede Addition zwischen Werten ähnlicher Größenordnung erfolgt.

Die Ganzzahlseite hat keine solche Grenze – MicroPython-Ganzzahlen haben beliebige Genauigkeit, genau wie die von CPython. Wo Sie die Wahl haben, bevorzugen Sie Ganzzahlarithmetik für alles, bei dem sich der Genauigkeitsverlust aufsummieren würde.