15. Gleitkomma-Arithmetik: Probleme und Einschränkungen¶

Gleitkommazahlen werden in Computerhardware als (binäre) Brüche der Basis 2 dargestellt. Zum Beispiel hat der Dezimalbruch

0.125

einen Wert 1/10 + 2/100 + 5/1000 , und auf die gleiche Weise hat der binäre Bruch

0.001

einen Wert 0/2 + 0/4 + 1/8. Diese beiden Brüche haben identische Werte, der einzigeder Unterschied besteht darin, dass der erste in der Bruchnotation der Basis 10 und der zweite in der Basis 2 geschrieben ist.

Leider können die meisten Dezimalbrüche nicht genau als Binärbrüche dargestellt werden. Dies hat zur Folge, dass die von Ihnen eingegebenen dezimalen Gleitkommazahlen im Allgemeinen nur durch die binären Gleitkommazahlen angenähert werden, die tatsächlich in der Maschine gespeichert sind.

Das Problem ist zunächst in Base 10 leichter zu verstehen. Betrachten Sie den Bruch1 / 3. Sie können dies als Basis 10-Bruch annähern:

0.3

oder besser

0.33

oder besser

0.333

und so weiter. Egal, wie viele Ziffern Sie aufschreiben möchten, das Ergebnis wird nie genau 1/3 sein, sondern eine immer bessere Annäherung an 1/3.

Auf die gleiche Weise kann der Dezimalwert 0.1 nicht genau als Basis-2-Bruch dargestellt werden, unabhängig davon, wie viele Ziffern der Basis 2 Sie verwenden möchten. In base2 ist 1/10 der sich unendlich wiederholende Bruch

0.0001100110011001100110011001100110011001100110011...

Stoppen Sie bei einer endlichen Anzahl von Bits, und Sie erhalten eine Annäherung. Bei den meisten heutigen Maschinen werden Gleitkommazahlen unter Verwendung eines binären Bruchs mit Angenähertzähler mit den ersten 53 Bits beginnend mit dem höchstwertigen Bit undmit dem Nenner als Zweierpotenz. Im Fall von 1/10 ist der binäre Bruch 3602879701896397 / 2 ** 55, der nahe, aber nicht genau dem wahren Wert von 1/10 entspricht.

Vielen Benutzern ist die Approximation aufgrund der Art und Weise, wie Werte angezeigt werden, nicht bekannt. Python gibt nur eine dezimale Annäherung an den wahren Dezimalwert der von der Maschine gespeicherten binären Annäherung aus. Auf den meisten Rechnern würde ifPython den wahren Dezimalwert der binären Approximation storedfor 0 .1, es müsste angezeigt werden

>>> 0.10.1000000000000000055511151231257827021181583404541015625

Das sind mehr Ziffern, als die meisten Leute nützlich finden, also hält Python die Anzahl der Ziffern überschaubar, indem stattdessen ein gerundeter Wert angezeigt wird

>>> 1 / 100.1

Denken Sie daran, auch wenn das gedruckte Ergebnis wie das exakter Wertvon 1/10 ist der tatsächlich gespeicherte Wert der nächste darstellbare binäre Bruch.

Interessanterweise gibt es viele verschiedene Dezimalzahlen, die das gleiche teilennächster ungefährer binärer Bruch. Zum Beispiel werden die Zahlen 0.1 und0.10000000000000001 und0.1000000000000000055511151231257827021181583404541015625 alle durch 3602879701896397 / 2 ** 55 angenähert. Da alle diese Dezimalwerte dieselbe Approximation haben, kann jeder von ihnen angezeigt werden, während die Invariante eval(repr(x)) == x .

Historisch gesehen würde die Python-Eingabeaufforderung und die eingebaute repr() -Funktion diejenige mit 17 signifikanten Ziffern wählen, 0.10000000000000001. Beginnend mitpython 3.1, Python (auf den meisten Systemen) kann jetzt die kürzeste davon auswählen und einfach 0.1 .

Beachten Sie, dass dies in der Natur des binären Gleitkommas liegt: Dies ist kein Bugin Python und auch kein Fehler in Ihrem Code. Sie sehen die gleiche Art von Unterschied in allen Sprachen, die die Gleitkommaarithmetik Ihrer Hardware unterstützen (obwohl einige Sprachen den Unterschied möglicherweise nicht standardmäßig oder in allen Ausgabemodi anzeigen).

Für eine angenehmere Ausgabe können Sie die Zeichenfolgenformatierung verwenden, um eine begrenzte Anzahl signifikanter Ziffern zu erzeugen:

>>> format(math.pi, '.12g') # give 12 significant digits'3.14159265359'>>> format(math.pi, '.2f') # give 2 digits after the point'3.14'>>> repr(math.pi)'3.141592653589793'

Es ist wichtig zu erkennen, dass dies im wahrsten Sinne eine Illusion ist: Sie runden einfach die Anzeige des wahren Maschinenwerts ab.

Eine Illusion kann eine andere erzeugen. Da beispielsweise 0,1 nicht genau 1/10 ist, ergibt das Summieren von drei Werten von 0,1 möglicherweise auch nicht genau 0,3:

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

Da 0,1 dem genauen Wert von 1/10 nicht näher kommen kann und 0,3 dem genauen Wert von 3/10 nicht näher kommen kann, runden Sie mitround() Funktion kann nicht helfen:

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

Obwohl die Zahlen nicht näher an ihre beabsichtigten exakten Werte herangeführt werden können, kann die Funktion round() zum Nachrunden nützlich sein, damit die Ergebnisse mit ungenauen Werten miteinander vergleichbar werden:

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

Binäre Gleitkomma-Arithmetik birgt viele Überraschungen wie diese. Das Problem mit „0.1“ wird unten im Abschnitt „Darstellungsfehler“ genauer erläutert. Siehe Die Gefahren von Gleitkommazur vollständigeren Darstellung anderer häufiger Überraschungen.

Wie es am Ende heißt: „Es gibt keine einfachen Antworten.“ Seien Sie dennoch nicht übermäßig vorsichtig mit Gleitkommazahlen! Die Fehler in Python-Float-Operationen werden von der Gleitkommahardware geerbt und liegen auf den meisten Computern in der Größenordnung von nmehr als 1 Teil in 2 ** 53 pro Operation. Das ist für die meisten Aufgaben mehr als ausreichend, aber Sie müssen bedenken, dass es sich nicht um Dezimalarithmetik handelt und dass bei jeder Float-Operation ein neuer Rundungsfehler auftreten kann.

Obwohl pathologische Fälle existieren, werden Sie bei den meisten gelegentlichen Verwendungen von Gleitkommaarithmetik das Ergebnis sehen, das Sie am Ende erwarten, wenn Sie die Anzeige Ihrer Endergebnisse einfach auf die Anzahl der erwarteten Dezimalstellen runden.str() reicht normalerweise aus, und für eine genauere Kontrolle siehe die Formatbezeichner der str.format()Methode in der Formatzeichenfolgensyntax.

Für Anwendungsfälle, die eine exakte Dezimaldarstellung erfordern, verwenden Sie das Moduldecimal, das Dezimalarithmetik implementiert, die für Rechnungsanwendungen und hochpräzise Anwendungen geeignet ist.

Eine andere Form der exakten Arithmetik wird durch das fractions Modul unterstützt, das Arithmetik basierend auf rationalen Zahlen implementiert (so können die Zahlen wie 1 / 3 genau dargestellt werden).

Wenn Sie ein starker Benutzer von Gleitkommaoperationen sind, sollten Sie sich das numerische Python-Paket und viele andere Pakete für mathematische und statistische Operationen ansehen, die vom SciPy-Projekt bereitgestellt werden. Siehe <https://scipy.org>.

Python bietet Tools, die in den seltenen Fällen hilfreich sein können, in denen Sie den genauen Wert eines float wirklich wissen möchten. Diefloat.as_integer_ratio() Methode drückt den Wert eines float als afraction:

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

Da das Verhältnis genau ist, kann es verwendet werden, um den ursprünglichen Wert verlustfrei wiederherzustellen:

>>> x == 3537115888337719 / 1125899906842624True

Die float.hex() drückt einen Float in hexadezimal (base16) aus und gibt wieder den genauen Wert an, der von Ihrem Computer gespeichert wurde:

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

Diese genaue hexadezimale Darstellung kann verwendet werden, um den Float-Wert genau zu rekonstruieren:

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

Da die Darstellung genau ist, ist sie nützlich, um Werte zuverlässig über verschiedene Python-Versionen hinweg zu portieren (plattformunabhängig) und Daten mit anderen Sprachen auszutauschen, die dasselbe Format unterstützen (z. B. Java und C99).

Ein weiteres hilfreiches Tool ist die math.fsum() Funktion, die hilft, Präzisionsverluste während der Summierung zu mindern. Es verfolgt „verlorene Ziffern“, wenn Werte zu einer laufenden Summe hinzugefügt werden. Das kann einen Unterschied in der Gesamtgenauigkeit machen, so dass sich die Fehler nicht bis zu dem Punkt ansammeln, an dem sie die endgültige Summe beeinflussen:

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

15.1. Darstellungsfehler¶

Dieser Abschnitt erläutert das Beispiel „0.1“ im Detail und zeigt, wie Sie selbst eine genaue Analyse solcher Fälle durchführen können. Grundlegende Vertrautheit mit binaryfloating-Point-Darstellung wird angenommen.

Darstellungsfehler bezieht sich auf die Tatsache, dass einige (die meisten)Dezimalbrüche nicht genau als binäre (Basis 2) Brüche dargestellt werden können.Dies ist der Hauptgrund, warum Python (oder Perl, C, C ++, Java, Fortran und viele andere) oft nicht die genaue Dezimalzahl anzeigt, die Sie erwarten.

Warum ist das so? 1/10 ist nicht genau als binärer Bruch darstellbar. Fast allemachines heute (November 2000) verwenden IEEE-754 Gleitkomma-Arithmetik, andalmost alle Plattformen Karte Python schwimmt IEEE-754 „double precision“. 754doppel enthalten 53 Bit Genauigkeit, so dass der Computer bei der Eingabe bestrebt ist, 0,1 in den nächstgelegenen Bruch der Form J / 2 ** N umzuwandeln, wobei J eine ganze Zahl ist, die genau 53 Bit enthält. Umschreiben

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

als

J ~= 2**N / 10

und unter Hinweis darauf, dass J genau 53 Bits hat (ist >= 2**52 aber< 2**53

), ist der beste Wert für N 56:

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

Das heißt, 56 ist der einzige Wert für N, der J mit genau 53 Bits verlässt. Der bestmögliche Wert für J ist dann dieser Quotient.:

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

Da der Rest mehr als die Hälfte von 10 ist, wird die beste Annäherung durch Aufrunden erhalten:

>>> q+17205759403792794

Daher ist die bestmögliche Annäherung an 1/10 in 754 doppelter Genauigkeit:

7205759403792794 / 2 ** 56

Das Teilen von Zähler und Nenner durch zwei reduziert den Bruch auf:

3602879701896397 / 2 ** 55

Beachten Sie, dass dies, da wir aufgerundet haben, tatsächlich etwas größer als 1/10 ist;wenn wir nicht aufgerundet hätten, wäre der Quotient etwas kleiner als1 / 10. Aber auf keinen Fall kann es genau 1/10 sein!

Der Computer „sieht“ also nie 1/10: Was er sieht, ist der genaue Bruch, der oben angegeben ist, die beste 754-Doppelannäherung, die er erhalten kann:

>>> 0.1 * 2 ** 553602879701896397.0

Wenn wir diesen Bruch mit 10 **55 multiplizieren, können wir den Wert bis zu55 Dezimalstellen:

>>> 3602879701896397 * 10 ** 55 // 2 ** 551000000000000000055511151231257827021181583404541015625

was bedeutet, dass die genaue Zahl im Computer gespeichert ist gleichder Dezimalwert 0.1000000000000000055511151231257827021181583404541015625.Anstatt den vollständigen Dezimalwert anzuzeigen, runden viele Sprachen (einschließlich älterer Versionen von Python) das Ergebnis auf 17 signifikante Stellen ab:

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

Die Module fractions und decimal machen diese Berechnungen einfach:

>>> 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'

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.