14.3. Fließkommazahlen - float#
Zunächst ist zu sagen, dass Sie für Gleit- bzw. Fließkommazahlen (engl. Floating Point Number) nicht das Komma ,
sondern den englischsprachigen Punkt .
verwenden.
Im Gegensatz zu ganzen Zahlen benötigen Fließkommazahlen immer die gleiche Anzahl an Bits, nämlich genau 64 Bit. Da stellt sich die Frage: Wie ist es unter diesen Bedingungen möglich Zahlen darzustellen, deren absoluter Wert sehr verschieden ist? Zum Beispiel können wir problemlos die Zahl
und die Zahl
als Fließkommazahl definieren:
small_float = 0.0000000001
large_float = 10000000.0
print(small_float)
print(large_float)
1e-10
10000000.0
1e-10
bedeutet hierbei \(1.0 \cdot 10^{-10}\).
Diese Darstellung liefert uns bereits Hinweise auf die Antwort.
Physiker*innen kennen das Problem der Zahlen aus sehr unterschiedlichen Skalen. Die Lichtgeschwindigkeit in km/h ist eine sehr große Zahl, wohingegen die Ladung eines Elektrons in Coulomb eine sehr kleine Zahl ist. Physiker*innen möchten ebenfalls in einem riesigen Zahlenbereich rechnen und dabei Zahlen kompakt notieren können. Deshalb haben sie die wissenschaftliche Notation eingeführt. Fließkommazahlen sind aus dieser Notation entstanden. In dieser Notation schreiben wir
als
und aus
wird
Ohne zu sehr ins Detail zu gehen, besteht eine Fließkommazahl float
aus Bits für die einzelnen Teile dieser Schreibweise:
das Vorzeichen (1 Bit),
die Mantisse (52 Bit) und
den Exponenten (11 Bit).
Für \(0.1 \cdot 10^8\) wäre das Vorzeichen gleich +, die Mantisse gleich 0.1 und der Exponent gleich 8.
Da der Computer jedoch im Binärsystem rechnet, verwendet er als Basis die 2 anstatt die 10. Nehmen wir die Zahl
Binär können wir die Zahl wie folgt ausdrücken:
Soweit so gut. Was passiert aber wenn jede Ziffer von 0 verschieden ist und sich endlos fortsetzt z.B. \(\pi = 3.14159265359 \ldots\) oder auch \(1/3 = 0.33333333333 \ldots\)? In diesem Fall wird die Zahl abgeschnitten sobald keine Bits mehr zur Verfügung stehen (und es wird auf die letzte darstellbare Stelle aufgerundet). Demnach ist eine Fließkommazahl immer nur eine gute Annäherung des echten Werts!!!
Fließkommazahlen sind Annäherungen
Eine Fließkommazahl ist eine gute Annäherung des exakten Werts den sie repräsentieren soll.
Weshalb folgende Rechnung nicht 0.3 ergibt, erklärt sich durch diese Annäherung in Kombination mit der Binärdarstellung!
0.2 + 0.1
0.30000000000000004
Denn es gilt:
Dieses Verhalten kann nicht nur zu kleinen Ungenauigkeiten, sondern zu großen Fehlern führen.
Folgender Code subtrahiert 20 mal \(1.0 \cdot 10^{-14}\) von \(1.0 \cdot 10^{10}\).
Doch verändern diese Subtraktionen x
nicht.
x = 1e10
epsilon = 1e-14
print(f'epsilon = {epsilon}')
print(f'x = {x} before subtraction')
for i in range(20):
x = x - epsilon
print(f'x = {x} after subtraction')
epsilon = 1e-14
x = 10000000000.0 before subtraction
x = 10000000000.0 after subtraction
Das zeigt, dass der (akkumulierte) Fehler theoretisch unendlich groß werden kann. Kritisch ist die Addition bzw. Subtraktion von Zahlen die in sehr unterschiedlichen Skalen liegen. Die Multiplikation und Division ist für Fließkommazahlen sehr viel ungefährlicher.
x = 1e10
epsilon = 1e-14
print(f'epsilon = {epsilon}')
print(f'x = {x} before multiplication')
x = x * epsilon
print(f'x = {x} after multiplication')
epsilon = 1e-14
x = 10000000000.0 before multiplication
x = 0.0001 after multiplication
Obiger Code liefert das korrekte Ergebnis von:
Ungenauigkeit der Fließkommazahlen
Prüfen Sie Fließkommazahlen niemals auf Gleichheit ==
.
Verwenden Sie stattdessen immer einen kleinen Bereich in dem die Zahl liegen sollte.
Besonders kritisch ist die Subtraktion (oder Addition und unterschiedliche Vorzeichen) von Zahlen, die sehr nahe beieinander liegen. Anstatt des korrekten Ergebnisses erhalten wir eine Zahl deren relative Abweichung vom richtigen Ergebnis weit daneben liegen kann:
x = 100123123123.101
y = 100123123123.102
abs_error = abs(0.001 - abs(x-y))
print(f'x = {x}')
print(f'y = {y}')
print(f'x-y = {x-y}')
print(f'relative error = {abs_error/0.001}')
print(f'absolute error = {abs_error}')
x = 100123123123.101
y = 100123123123.102
x-y = -0.001007080078125
relative error = 0.007080078124999979
absolute error = 7.080078124999979e-06
Sowohl der absolute als auch der relative Fehler sind groß. Wir können den absoluten Fehler durch weitere Operationen beliebig erhöhen:
x = 100123123123.101
y = 100123123123.102
a = 100000000
sub = x-y
result = sub * a
abs_error = abs(0.001 - abs(x-y))*a
print(f'x = {x}')
print(f'y = {y}')
print(f'result = {result}')
print(f'relative error = {abs_error/100000}')
print(f'absolute error = {abs_error}')
x = 100123123123.101
y = 100123123123.102
result = -100708.0078125
relative error = 0.007080078124999979
absolute error = 708.007812499998
Das kann soweit gehen, dass alle verbleibenden Bits sich aus Rundungsfehlern in der Berechnung ergeben. Diesen Effekt nennt Auslöschung.
Im Artikel What Every Computer Scientist Should Know About Floating-Point Arithmetik finden Sie einen ausführlichen Diskurs zu diesem Thema, welcher allerdings weit über diesen Kurs hinausgeht.
(Modellierung von Geldbeträgen)
Sie wollen eine Finanzsoftware entwickeln. Sollten Sie die Geldbeträge als Fließkommazahlen abspeichern? Begründen Sie Ihre Antwort.
Solution to Exercise 14.1 (Modellierung von Geldbeträgen)
Im Finanzwesen arbeiten wir mit Gleitkommazahlen mit der Basis 10. Selbst wenn diese Zahlen nur wenige Nachkommastellen besitzen können diese im Binärsystem unendlich viele Nachkommastellen besitzen. Da wir im Bankwesen exakte Werte benötigen, eignen Fließkommazahlen nicht. Das Modul decimal bietet die nötige Funktionalität.