Das Symptom
Der Taschenrechner im Kopf sagt 0,3. Das Programm sagt:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
In der Tabelle oder im Warenkorb sieht es harmloser und ärgerlicher aus: Drei
Posten, die zusammen 39,99 Euro ergeben müssten, stehen intern als
39.989999999999995 – und wer sie abschneidet statt zu runden, bucht 39,98. Die
Umsatzliste weicht dann um einen Cent vom Kontoauszug ab, und niemand findet den
fehlenden Betrag, weil er nirgends liegt.
Die „Layer 8“-Wahrheit
Der Computer hat recht, die Erwartung war falsch. Fließkommazahlen speichern
Brüche im Zweiersystem, und im Zweiersystem ist ein Zehntel genauso unendlich
periodisch wie ein Drittel im Zehnersystem: 0,0001100110011… ohne Ende. Nach 53
Bit Präzision – der doppelten Genauigkeit nach IEEE 754, die praktisch jeder
Prozessor seit den 2000er-Jahren ausführt – bricht die Darstellung ab. Was
gespeichert wird, ist eine sehr gute Näherung an 0,1. Nur eben keine 0,1. Zwei
Näherungen addiert ergeben eine dritte Näherung, und die trifft 0,3 nicht auf den
letzten Bit genau. Das ist kein Bug in Python, nicht in Excel und nicht im
Prozessor: Es ist dieselbe Norm, die auch C, Java und JavaScript benutzen, und sie
tut exakt das, was in ihr steht.
Der eigentliche Layer-8-Anteil ist der Reflex, das Problem mit round()
zuzukleistern. Runden macht die Anzeige hübsch und den Fehler unsichtbar, aber
nicht kleiner – und round() selbst hat zwei dokumentierte Eigenheiten, die
nachts noch für Diskussionen sorgen: Bei genau gleichem Abstand rundet Python zur
geraden Zahl, und round(2.675, 2) liefert 2.67, weil 2,675 nie im Speicher
stand. Wer mit Geld rechnet, hört deshalb auf, Geld als Kommazahl zu behandeln:
intern ganze Cent, oder ein Dezimaltyp wie decimal, der ausdrücklich für
Buchhaltung gedacht ist. Gerundet wird einmal, ganz am Schluss, für die Augen des
Lesers. Alles andere ist der Versuch, mit einer Näherung eine Bilanz zu führen.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.Erst nachrechnen, dann fluchen
Bevor du die Bibliothek verdächtigst, lass dir das Ergebnis roh ausgeben. Die zusätzlichen Stellen sind kein Anzeigefehler, sie sind der tatsächlich gespeicherte Wert. Der Vergleich auf Gleichheit scheitert deshalb völlig zu Recht.
python3 -c "print(0.1 + 0.2); print(0.1 + 0.2 == 0.3)"
# 0.30000000000000004
# False
2.Den Beleg im Alltag suchen: drei Positionen, ein Cent daneben
Das Phänomen bleibt kein Laborbeispiel. Sobald mehrere Beträge zusammenkommen, wandert die Abweichung in die zweite Nachkommastelle – genau dorthin, wo bei Geld die Wahrheit wohnt.
python3 -c "print(19.99 + 0.01 + 19.99); print(sum([19.99, 5.01, 3.30]))"
# 39.989999999999995
# 28.299999999999997
3.Geld in ganzen Cent rechnen
Die robusteste Lösung ist die langweiligste: intern nur Ganzzahlen speichern, nämlich Cent statt Euro. Ganzzahlen kennen keine Rundungsfehler, und die Kommastelle entsteht erst bei der Ausgabe. Datenbanken und Kassensysteme machen es seit Jahrzehnten so.
python3 -c "s = 1999 + 1 + 1999; print(s, f'{s // 100},{s % 100:02d} Euro')"
# 3999 39,99 Euro
4.Wo es dezimal bleiben muss: das Modul decimal
Wenn Prozentsätze, Steuersätze oder Wechselkurse ins Spiel kommen, wird die Cent-Ganzzahl unhandlich. Dann nimmt man einen Dezimaltyp. Die Python-Dokumentation empfiehlt genau dafür das Modul `decimal`, das eine für Buchhaltung geeignete Dezimalarithmetik umsetzt. Wichtig: die Werte als Zeichenkette übergeben, sonst füttert man den Dezimaltyp mit einer bereits verfälschten Fließkommazahl.
python3 -c "from decimal import Decimal; print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))"
# True
5.Fließkommazahlen nie auf Gleichheit prüfen
Wo Fließkomma bleiben darf – Messwerte, Geometrie, Physik –, ersetzt man den Gleichheitstest durch einen Nähetest. Die Standardbibliothek bringt ihn mit, man muss keine eigene Epsilon-Konstante erfinden.
python3 -c "import math; print(math.isclose(0.1 + 0.2, 0.3))"
# True
6.Erst bei der Ausgabe runden – und wissen, was round() tut
Gerundet wird zum Schluss, nie zwischendurch. Und man sollte die zwei dokumentierten Eigenheiten kennen: Bei gleichem Abstand rundet Python zur geraden Zahl, und `round(2.675, 2)` ergibt 2.67 statt 2.68, weil 2,675 gar nicht exakt im Speicher steht.
python3 -c "print(round(0.5), round(1.5), round(2.5)); print(round(2.675, 2))"
# 0 2 2
# 2.67