14. Fließkomma-Arithmetik: Probleme und Einschränkungen

Fließkommazahlen werden in der Hardware des Computers als Brüche mit der Basis 2 (binär) dargestellt. Beispielsweise hat folgende Dezimalzahl

0.125

den Wert 1/10 + 2/100 + 5/1000; die entsprechende Binärzahl

0.001

hat den Wert 0/2 + 0/4 + 1/8. Diese beiden Brüche haben einen identischen Wert, der einzige wirkliche Unterschied ist, dass der erste als Bruch zur Basis 10 und der zweite als Bruch zur Basis 2 geschrieben wurde.

Leider können die meisten Dezimalbrüche nicht exakt als Binärbruch dargestellt werden. Eine Konsequenz daraus ist, dass im Allgemeinen die als dezimale Fließkommazahlen eingegebenen Werte nur durch die binären Fließkommazahlen angenähert werden können, die eigentlich von dem Computer gespeichert werden.

Das Problem ist zunächst einfacher im Dezimalsystem zu verstehen. Nehmen wir beispielsweise den Bruch 1/3. Man kann ihn in Dezimaldarstellung folgendermaßen annähern

0.3

oder, besser

0.33

oder noch besser,

0.333

und so weiter. Egal wie viele Stellen man schreibt, dass Resultat wird niemals exakt 1/3, aber es wird sich stetig 1/3 annähern.

Äquivalent kann, egal wie viele Stellen mit der Basis 2 man verwendet, die Dezimalzahl 0.1 niemals exakt als Binärbruch dargestellt werden. Im Binärsystem ist 1/10 die periodische Binärzahl:

0.0001100110011001100110011001100110011001100110011...

Hält man nach einer endlichen Zahl Bits an, erhält man eine Annäherung. In den meisten Rechnern werden heute Fließkommazahlen als Binärbrüche, mit dem Zähler in den ersten 53 Bits (beginnend mit dem höchstwertigsten Bit), gefolgt von dem Nenner als Potenz von Zwei, dargestellt. Im Fall von 1/10 lautet der Binärbruch 3602879701896397 / 2 ** 55, was in etwa, aber nicht exakt, dem echten Wert von 1/10 entspricht.

Viele Benutzer sind sich durch die angezeigten Werte der Problematik nicht bewusst. Python zeigt nur eine dezimale Annäherung an den echten Dezimalwert an, der im Rechner gespeichert wird. Wenn Python den echten Dezimalwert zur gespeicherten binären Annäherung an 0,1 anzeigen würde, müsste es folgendes anzeigen

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Das sind mehr Stellen als den meisten Leuten lieb ist, deshalb hält Python die Anzahl der Stellen überschaubar, indem es stattdessen einen gerundeten Wert anzeigt

>>> 1 / 10
0.1

Zur Erinnerung - auch wenn der angezeigte Wert wie der exakte Wert von 1/10 aussieht - ist der eigentlich gespeicherte Wert, der nächstgelegene Binärbruch.

Interessanterweise gibt es viele verschiedene Dezimalzahlen welche die selbe beste Approximation durch einen Binärbruch haben. Beispielsweise werden 0.1, 0.10000000000000001 und 0.1000000000000000055511151231257827021181583404541015625 alle mit 3602879701896397 / 2 ** 55 angenähert. Da all diese Dezimalwerte die selbe Approximation haben, könnte jeder von ihnen angezeigt werden und immer noch die Bedingung eval(repr(x)) == x erfüllen.

In der Vergangenheit wählten der Python-Prompt und die eingebaute Funktion repr() diejenige mit 17 signifikanten Stellen: 0.10000000000000001. Seit Python 3.1 ist Python (auf den meisten Systemen) dazu fähig, die kürzeste Darstellung zu wählen und einfach 0.1 anzuzeigen.

Das Verhalten liegt in der Natur der Fließkomma-Darstellung im Binärsystem: es ist kein Fehler in Python und auch kein Fehler in deiner Software. Man sieht dieses Problem in allen Sprachen, die die Fließkomma-Darstellung der Hardware unterstützen (obwohl manche Sprachen den Unterschied nicht standardmäßig oder in allen Anzeigemodi anzeigen).

Für eine schönere Ausgabe kann man String Formatierung nutzen, um die Anzahl der ausgegebenen signifikanten Ziffern zu beschränken.

>>> format(math.pi, '.12g')  # 12 signifikante Stellen angeben
'3.14159265359'
>>> format(math.pi, '.2f')   # 2 Nachkommastellen angeben
'3.14'

Es ist wichtig sich zu verinnerlichen, dass dies in Wahrheit eine Illusion ist - man rundet einfach die Darstellung des echten Maschinenwertes.

Diese Illusion erzeugt unter Umständen eine weitere. Da beispielsweise 0.1 nicht exakt 1/10 ist, ist die Summe von dreimal 0.1 nicht exakt 0.3:

>>> .1 + .1 + .1 == .3
False

Da 0.1 außerdem nicht näher an den exakten Wert von 1/10 und 0.3 heranreichen kann hilft auch vorheriges Runden mit round() nichts:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

Obwohl die Zahlen nicht besser an ihren gedachten exakten Wert angenähert werden können, kann die Funktion round() nützlich für das nachträgliche Runden, so dass die ungenauen Ergebnisse vergleichbar zueinander werden:

>>> round(.1 + .1 + .1, 1) == round(.3, 1)
True

Binäre Fließkommaarithmetik sorgt noch für einige Überraschungen wie diese. Das Problem mit “0.1” ist im Abschnitt “Darstellungsfehler” weiter unten detailliert beschrieben. Dazu sei auf The Perils of Floating Point für eine umfassendere Liste von üblichen Problemen verwiesen.

Wie schon dort gegen Ende des Textes gesagt wird: “Es gibt keine einfachen Antworten.” Trotzdem sollte man nicht zögerlich bei dem Einsatz von Fließkommazahlen sein! Die Fehler in Python-Fließkommaoperationen sind Folgefehler der Fließkomma-Hardware und liegt auf den meisten Maschinen in einem Bereich der geringer als 1 zu 2**53 pro Operation ist. Das ist mehr als ausreichend für die meisten Anwendungen, aber man muss sich in Erinnerung halten das es sich nicht um Dezimal-Arithmetik handelt und dass jede Operation mit einer Fließkommazahl einen neuen Rundungsfehler enthalten kann.

Von einigen pathologischen Fällen abgesehen, erhält man in den meisten existierenden Fällen, für die gängigsten Anwendungen von Fließkommazahlen das erwartete Ergebnis, wenn man einfach die Anzeige des Ergebnisses auf die Zahl der Dezimalstellen rundet, die man erwartet. str() genügt meist, für eine feinere Kontrolle kann man sich str.format() mit den Formatierungsoptionen in Format String Syntax anschauen.

Für Anwendungsfälle, die eine exakte dezimale Darstellung benötigen, kann das Modul decimal verwendet werden, welches Dezimal-Arithmetik implementiert, die für Buchhaltung und andere Anwendungen, die eine hohe Präzision erfordern, geeignet ist.

Eine andere Form exakter Arithmetik wird von dem Modul fractions bereitgestellt, welche eine Arithmetik implementiert, die auf rationalen Zahlen basiert (so dass Zahlen wie 1/3 exakt abgebildet werden können).

Wenn man im größeren Umfang mit Fließkommazahlen zu tun hat, sollte man einen Blick auf Numerical Python und die vielen weitere Pakete für mathematische und statistische Operationen die vom SciPy-Projekt bereitgestellt werden anschauen.

Python verfügt außerdem über ein Werkzeug für die seltenen Fälle, in denen man wirklich den exakten Wert des floats wissen will. Die Methode float.as_integer_ratio() gibt den Wert der Fließkommazahl als Bruch zurück:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

Da dieser Bruch exakt ist, kann er benutzt werden, um ohne Verluste den originalen Wert wiederherzustellen:

>>> x == 3537115888337719 / 1125899906842624
True

Die Metode float.hex() stellt die Fließkommazahl hexadezimal (Basis 16) dar und gibt ebenfalls den exakten im Rechner gespeicherten Wert zuück:

>>> x.hex()
'0x1.921f9f01b866ep+1'

Diese präzise hexadezimale Darstellung kann benutzt werden, um den originalen Wert exakt wiederherzustellen:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

Da diese Darstellung exakt ist, kann sie genutzt werden, um Daten zwischen verschiedenen Versionen von Python (plattformunabhängig) und zwischen verschiedenen anderen Sprachen, die dieses Format unterstützen (wie z.B. Java und C99), auszutauschen.

Ein weiteres hilfreiches Werkzeug ist die Funktion math.fsum(), welche den Genauigkeitsverlust beim Summieren verringert. Sie registriert die “verlorenen Ziffern” als Werte, die zu einer Summe addiert werden. Dies kann die Gesamtgenauigkeit dahingehend beeinflussen, dass die Fehler sich nicht zu einer Größe summieren, die das Endergebnis beeinflusst:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

14.1. Darstellungsfehler

Dieser Abschnitt erklärt das “0.1” Beispiel im Detail und zeigt wie man selbstständig eine exakte Analyse dieser Fälle durchführen kann. Ein grundlegendes Verständnis der binären Fließkomma-Darstellung wird vorausgesetzt.

Der Begriff Darstellungsfehler verweist auf den Umstand das manche (die meisten sogar) Dezimalbrüche nicht exakt als Binärbrüche (Basis 2) dargestellt werden können. Dies ist der Hauptgrund warum Python (oder Perl, C, C++, Java, Fortran, und viele andere) oft nicht das erwartete Ergebnis anzeigen.

Warum ist das so? 1/10 ist nicht exakt als Binärbruch darstellbar. Fast alle heutigen Rechner (November 2000) benutzen die IEEE-754 Fließkommaarithmetik und wie fast alle Plattformen, bildet Python floats als IEEE-754 “double precision” ab. IEEE-754 doubles sind auf 53 Bits genau, so dass sich der Computer bemüht, 0.1 mit einem Bruch der Form J/2**N bestmöglich anzunähern, wobei J eine 53 Bit breite Ganzzahl ist. Schreibt man:

1 / 10 ~= J / (2**N)

als

J ~= 2**N / 10

und erinnert sich daran das J genau 53 Bit breit ist (d. h. >= 2**52 und < 2**53), ergibt sich als bester Wert für N 56:

>>> 2**52 <=  2**56 // 10  < 2**53
True

Das heißt, 56 ist der einzige Wert für N, wenn J auf 53 Bits beschränkt ist. Der bestmögliche Wert für J ist dann der gerundete Quotient:

>>> q, r = divmod(2**56, 10)
>>> r
6

Da der Rest mehr als die Hälfte von 10 beträgt, wird die beste Annäherung durch Aufrunden ermittelt:

>>> q+1
7205759403792794

Aus diesem Grund ist die bestmögliche Approximation von 1/10 als “IEEE-754 double precision”:

7205759403792794 / 2 ** 56

Kürzt man Zähler und Nenner mit 2, ergibt sich folgender Bruch:

3602879701896397 / 2 ** 55

Man beachte, dass, da aufgerundet wurde, dieser Wert in Wahrheit etwas größer ist als 1/10; hätte man nicht aufgerundet, wäre der Bruch ein wenig kleiner als 1/10. Aber in keinen Fall wäre er exakt 1/10!

Der Rechner bekommt also nie 1/10 zu sehen: Was er sieht, ist der exakte oben dargestellte Bruch, die beste “IEEE-754 double” Approximation, die es gibt:

>>> 0.1 * 2 ** 55
3602879701896397.0

Wenn dieser Bruch mit 10**55 multipliziert wird, kann man sich diesen Wert bis auf 55 Dezimalstellen anzeigen lassen:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

was bedeutet, dass der exakte Wert der im Rechner gespeichert würde, in etwa dem Dezimalwert 0.1000000000000000055511151231257827021181583404541015625 entspricht. Anstatt den ganzen Dezimalwert anzuzeigen runden viele Sprachen (inklusive älterer Versionen von Python) das Ergebnis auf 17 signifikante Stellen:

>>> format(0.1, '.17f')
'0.10000000000000001'

Die Module fractions und decimal vereinfachen diese Rechnungen:

>>> from decimal import Decimal
>>> from fractions import Fraction

>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)

>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'