mitmario.dev

Rechnen mit Geld

Python Sandbox 4 Min Lesezeit 4 BeispieleLektion 5 von 7

Auf einer Rechnung muss jeder Cent stimmen, und round(2.675, 2) ergibt 2.67. Dass ab und zu eine Zahl wie 0.30000000000000004 im Terminal steht, weißt du seit dem ersten Abschnitt. Bisher hieß es, man rundet einfach beim Ausgeben, dann sieht man es nicht. Für eine Durchschnittstemperatur reicht das. Für eine Rechnung nicht. Dort gilt eine Regel, die nicht Python festlegt, sondern der Kaufmann, und die lautet: Ab einer 5 wird aufgerundet.

Zwei Probleme mit Kommazahlen

Zwei Probleme mit Kommazahlen
print(1.10 + 2.20)
print(1.10 + 2.20 == 3.30)

print(round(0.5), round(1.5), round(2.5))
print(round(2.675, 2))

Das erste Problem kennst du aus Lektion 2.2. Eine Kommazahl (float) wird im Zweiersystem gespeichert, und 1.10 geht dort nicht genau auf. Zusammen mit 2.20 kommt 3.3000000000000003 heraus, und der Vergleich mit 3.30 ergibt False. Du willst prüfen, ob eine Rechnung aufgeht, und Python sagt Nein, obwohl jeder Taschenrechner Ja sagt.

Das zweite Problem ist neu und tückischer. round() rundet eine 5 nicht immer auf, sondern zur nächsten geraden Zahl. round(0.5) ist 0, round(1.5) ist 2 und round(2.5) ist wieder 2.

Das ist keine Schlamperei, sondern Absicht. Wenn man tausend Werte so rundet, wird ungefähr gleich oft auf- wie abgerundet, und die Summe verrutscht nicht. Auf einer Rechnung ist das Ergebnis trotzdem falsch, weil dort ab 5 aufgerundet wird.

Beim round(2.675, 2) vom Anfang kommt beides zusammen, und heraus kommt 2.67. Mit dem Runden zur geraden Zahl hat das hier nicht einmal etwas zu tun, denn eine 5 kommt bei round gar nicht an. Gespeichert ist 2.67499999…, ein winziges Stück darunter, und das wird ganz korrekt abgerundet.

Decimal aus Text

Decimal aus Text
from decimal import Decimal

print(Decimal("1.10") + Decimal("2.20"))
print(Decimal("1.10") + Decimal("2.20") == Decimal("3.30"))

print(Decimal("2.675"))
print(Decimal(2.675))

Im Modul decimal steckt eine eigene Art von Zahl, Decimal (Dezimalzahl). Damit wird im Zehnersystem gerechnet, so wie auf Papier. 1.10 und 2.20 gehen dann genau auf, zu 3.30, und der Vergleich ergibt True. Die Nachkommastellen bleiben so, wie sie hineingekommen sind. Deshalb steht dort 3.30 und nicht 3.3.

Wichtig ist, woraus so eine Zahl entsteht. Decimal("2.675") bekommt Text und ist genau 2.675. Decimal(2.675) bekommt eine Kommazahl, und die war schon ungenau, bevor sie bei Decimal ankam. In der letzten Zeile siehst du, was wirklich im Speicher stand, mit fünfzig Nachkommastellen.

Ein Decimal ist also nur so genau wie das, woraus es gemacht wird, und deshalb entsteht es aus Text. Werte aus einer Datei sind sowieso Text. Die gibst du direkt weiter, ohne den Umweg über float().

Das gilt auch für eine Konstante. MWST = 0.19 ist genauso eine Kommazahl, und Decimal(MWST) erbt ihre Ungenauigkeit. Genau wird es erst mit MWST = Decimal("0.19"), aus Text.

Kaufmännisch gerundet

Kaufmännisch gerundet
from decimal import Decimal, ROUND_HALF_UP

CENT = Decimal("0.01")
print(Decimal("2.675").quantize(CENT, rounding=ROUND_HALF_UP))
print(Decimal("2.665").quantize(CENT, rounding=ROUND_HALF_UP))
print(round(Decimal("2.665"), 2))

Ein Decimal wird mit quantize (auf ein Maß bringen) gerundet. Das Maß ist ein Decimal mit so vielen Stellen, wie herauskommen sollen, für Cent also Decimal("0.01"). Dahinter steht die Rundungsregel. ROUND_HALF_UP heißt ab der Hälfte auf, also das, was man in der Schule als kaufmännisches Runden gelernt hat. Aus 2.675 werden 2.68, und aus 2.665 werden 2.67.

In der letzten Zeile tappen auch die in die Falle, die Decimal schon richtig benutzen, weil round() auf ein Decimal zwar genau rechnet, aber weiter zur geraden Zahl rundet. Aus 2.665 werden so 2.66. Dasselbe passiert mit quantize ohne rounding und mit :.2f im f-String.

Bei Geld steht deshalb besser immer quantize mit ausdrücklicher Regel da, auch wenn es länger ist. Das Maß liegt dafür einmal als CENT bereit.

Decimal mal Kommazahl

Decimal mal Kommazahl
from decimal import Decimal

preis = Decimal("19.99")
print(preis * 3)
print(preis * 1.19)

Mit ganzen Zahlen lässt sich ein Decimal ohne Weiteres verrechnen, preis * 3 sind 59.97. Mit einer Kommazahl dagegen nicht. preis * 1.19 endet mit TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'.

Das ist Absicht. Die Kommazahl hätte ihre Ungenauigkeit gleich mit in die Rechnung geschleppt, und statt still einen falschen Cent zu liefern, weigert sich Python lieber. Richtig heißt es preis * Decimal("1.19").

Überall braucht man das nicht. Für eine Durchschnittstemperatur, eine Strecke oder einen Anteil in Prozent ist float genau genug, und round() darf runden, wie es will. Decimal lohnt sich da, wo am Ende ein Betrag steht, der auf den Cent stimmen und einer bestimmten Regel folgen muss. Also bei Rechnungen, Steuern und Abrechnungen.

Was die Aufgabe will

Eine Handwerkerrechnung mit Arbeitszeit, Kabel und Anfahrt, dazu Netto, Umsatzsteuer und Brutto. Das Programm rechnet mit Kommazahlen und round(), und am Ende fehlt ein Cent, je nach Rechnung an einer anderen Stelle. Mit Decimal und kaufmännischem Runden stimmt die Rechnung auf den Cent.

Zum Mitnehmen

Beim Geld machen Kommazahlen an zwei Stellen Ärger. 1.10 + 2.20 ergibt nicht genau 3.30, und round() rundet eine 5 zur geraden Zahl. Genau rechnet Decimal aus dem Modul decimal, aber nur, wenn es aus Text entsteht, also Decimal("2.675") und nie Decimal(2.675). Kaufmännisch gerundet wird mit quantize(Decimal("0.01"), rounding=ROUND_HALF_UP).

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.