Warum sieht der Gleitkommawert von 4 * 0,1 in Python 3 gut aus, 3 * 0.1 jedoch nicht?

158

Ich weiß, dass die meisten Dezimalstellen keine exakte Gleitkommadarstellung haben ( Ist die Gleitkomma-Mathematik gebrochen? ).

Aber ich verstehe nicht, warum gut 4*0.1gedruckt wird 0.4, aber 3*0.1nicht, wenn beide Werte tatsächlich hässliche Dezimaldarstellungen haben:

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Aivar
quelle
7
Weil einige Zahlen genau dargestellt werden können und andere nicht.
Morgan Thrapp
58
@ MorganThrapp: Nein, ist es nicht. Das OP fragt nach der eher willkürlich aussehenden Formatierungsoption. Weder 0,3 noch 0,4 können exakt im binären Gleitkomma dargestellt werden.
Bathseba
42
@BartoszKP: Nachdem das Dokument mehrmals gelesen, es erklärt nicht , warum Python anzeigt , 0.3000000000000000444089209850062616169452667236328125wie 0.30000000000000004und 0.40000000000000002220446049250313080847263336181640625wie .4auch wenn sie die gleiche Genauigkeit zu haben scheinen, und damit die Frage nicht beantworten.
Mooing Duck
6
Siehe auch stackoverflow.com/questions/28935257/… - Ich bin etwas irritiert darüber, dass es als Duplikat geschlossen wurde, aber dieses hat es nicht.
Random832
12
Wieder geöffnet, gehen Sie bitte nicht in der Nähe dieser als Duplikat von „point math gebrochen schwimmt“ .
Antti Haapala

Antworten:

301

Die einfache Antwort ist, weil 3*0.1 != 0.3aufgrund eines Quantisierungsfehlers (Rundungsfehlers) (während das 4*0.1 == 0.4Multiplizieren mit einer Zweierpotenz normalerweise eine "exakte" Operation ist).

Sie können die .hexMethode in Python verwenden, um die interne Darstellung einer Zahl anzuzeigen (im Grunde genommen den exakten binären Gleitkommawert anstelle der Basis-10-Näherung). Dies kann helfen zu erklären, was unter der Haube vor sich geht.

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0,1 ist 0x1,999999999999a mal 2 ^ -4. Das "a" am Ende bedeutet die Ziffer 10 - mit anderen Worten, 0,1 im binären Gleitkomma ist sehr geringfügig größer als der "exakte" Wert von 0,1 (da das endgültige 0x0,99 auf 0x0a aufgerundet wird). Wenn Sie dies mit 4 multiplizieren, einer Zweierpotenz, verschiebt sich der Exponent nach oben (von 2 ^ -4 auf 2 ^ -2), aber die Zahl bleibt ansonsten unverändert 4*0.1 == 0.4.

Wenn Sie jedoch mit 3 multiplizieren, vergrößert sich der kleine Unterschied zwischen 0x0,99 und 0x0a0 (0x0,07) zu einem 0x0,15-Fehler, der an der letzten Position als einstelliger Fehler angezeigt wird. Dies führt dazu, dass 0,1 * 3 geringfügig größer als der gerundete Wert von 0,3 ist.

Der Float von Python 3 reprist so konzipiert, dass er rund ausgelöst werden kann. Das heißt, der angezeigte Wert sollte genau in den ursprünglichen Wert konvertierbar sein. Daher kann es nicht angezeigt werden 0.3und 0.1*3genau die gleiche Art und Weise, oder die beiden unterschiedlichen Zahlen würden die gleiche nach Round-Tripping enden. Folglich zeigt die Python 3- reprEngine eine mit einem leichten offensichtlichen Fehler an.

nneonneo
quelle
25
Dies ist eine erstaunlich umfassende Antwort, danke. (Insbesondere danke für das Zeigen .hex(); ich wusste nicht, dass es existiert.)
NPE
21
@supercat: Python versucht, die kürzeste Zeichenfolge zu finden , die auf den gewünschten Wert gerundet wird , unabhängig davon, was gerade passiert. Offensichtlich muss der ausgewertete Wert innerhalb von 0,5 ulp liegen (oder er würde auf etwas anderes runden), aber in mehrdeutigen Fällen sind möglicherweise mehr Ziffern erforderlich. Der Code ist sehr knorrig, aber wenn Sie einen Blick darauf werfen
möchten
2
@supercat: Immer die kürzeste Saite, die innerhalb von 0,5 ulp liegt. ( Streng innerhalb, wenn wir einen Float mit ungeradem LSB betrachten, dh die kürzeste Saite, mit der er mit Round-Tie-to-Even funktioniert). Alle Ausnahmen sind ein Fehler und sollten gemeldet werden.
Mark Dickinson
7
@ MarkRansom Sicherlich haben sie etwas anderes verwendet, als eweil das bereits eine hexadezimale Ziffer ist. Vielleicht pfür Macht statt Exponent .
Bergi
11
@Bergi: Die Verwendung von pin diesem Zusammenhang geht (zumindest) auf C99 zurück und erscheint auch in IEEE 754 und in verschiedenen anderen Sprachen (einschließlich Java). Als float.hexund float.fromheximplementiert wurden (von mir :-), kopierte Python lediglich die bis dahin etablierte Praxis. Ich weiß nicht, ob die Absicht für "Power" "p" war, aber es scheint eine gute Möglichkeit zu sein, darüber nachzudenken.
Mark Dickinson
75

repr(und strin Python 3) gibt so viele Ziffern aus, wie erforderlich sind, um den Wert eindeutig zu machen. In diesem Fall das Ergebnis der Multiplikation3*0.1 nicht der Wert, der 0,3 am nächsten kommt (0x1,33333333333333p-2 in hex), sondern tatsächlich ein LSB höher (0x1,3333333333333p-2), sodass mehr Ziffern erforderlich sind, um ihn von 0,3 zu unterscheiden.

Auf der anderen Seite, die Multiplikation 4*0.1 hat den nächsten Wert auf 0,4 (0x1.999999999999ap-2 in hex) erhalten, so dass es keine zusätzlichen Stellen nicht brauchen nicht.

Sie können dies ganz einfach überprüfen:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

Ich habe oben die Hex-Notation verwendet, weil sie schön und kompakt ist und den Bitunterschied zwischen den beiden Werten zeigt. Sie können dies selbst tun, indem Sie z (3*0.1).hex(). Wenn Sie sie lieber in ihrer ganzen Dezimalpracht sehen möchten, können Sie loslegen:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Mark Ransom
quelle
2
(+1) Schöne Antwort, danke. Denken Sie, dass es sich lohnen könnte, den Punkt "nicht der nächstgelegene Wert" durch Einbeziehung des Ergebnisses von 3*0.1 == 0.3und zu veranschaulichen 4*0.1 == 0.4?
NPE
@NPE Ich hätte das sofort tun sollen, danke für den Vorschlag.
Mark Ransom
Ich frage mich, ob es sich lohnen würde, die genauen Dezimalwerte der nächsten "Doppel" auf 0,1, 0,3 und 0,4 zu notieren, da viele Leute kein Gleitkomma-Hex lesen können.
Supercat
@supercat du machst einen guten Punkt. Das Einfügen dieser super großen Doppelbilder in den Text würde ablenken, aber ich dachte über eine Möglichkeit nach, sie hinzuzufügen.
Mark Ransom
25

Hier ist eine vereinfachte Schlussfolgerung aus anderen Antworten.

Wenn Sie ein Float in der Python-Befehlszeile überprüfen oder drucken, durchläuft es die Funktion, mit reprder die Zeichenfolgendarstellung erstellt wird.

Ab Version 3.2, Pythons strundrepr ein komplexes Rundungsschema verwenden, die gut aussehenden Dezimalzahlen wenn möglich bevorzugt, aber mehr Stellen Verwendungen , bei denen erforderlich Garantie bijektiv (one-to-one) Zuordnung zwischen Schwimmer und ihren String - Darstellungen.

Dieses Schema garantiert, dass der Wert von repr(float(s))für einfache Dezimalstellen gut aussieht, auch wenn sie nicht genau als Gleitkommazahlen dargestellt werden können (z. B. wann s = "0.1").

Gleichzeitig garantiert es, dass float(repr(x)) == xfür jeden Schwimmer giltx

Aivar
quelle
2
Ihre Antwort ist für Python-Versionen> = 3.2 korrekt, wobei strund reprfür Floats identisch sind. Hat für Python 2.7 reprdie Eigenschaften, die Sie identifizieren, ist aber strviel einfacher - es berechnet einfach 12 signifikante Ziffern und erzeugt eine darauf basierende Ausgabezeichenfolge. Für Python <= 2.6 basieren beide reprund strauf einer festen Anzahl signifikanter Ziffern (17 für repr, 12 für str). (Und niemand kümmert sich um Python 3.0 oder Python 3.1 :-)
Mark Dickinson
Danke @MarkDickinson! Ich habe Ihren Kommentar in die Antwort aufgenommen.
Aivar
2
Beachten Sie, dass die Rundung von der Shell stammt, reprdaher wäre das Verhalten von Python 2.7 identisch ...
Antti Haapala
5

Nicht wirklich spezifisch für die Implementierung von Python, sollte aber für alle Float-to-Decimal-String-Funktionen gelten.

Eine Gleitkommazahl ist im Wesentlichen eine Binärzahl, jedoch in wissenschaftlicher Notation mit einer festen Grenze signifikanter Zahlen.

Die Umkehrung einer Zahl mit einem Primzahlfaktor, der nicht mit der Basis geteilt wird, führt immer zu einer wiederkehrenden Punktpunktdarstellung. Zum Beispiel hat 1/7 einen Primfaktor 7, der nicht mit 10 geteilt wird, und hat daher eine wiederkehrende Dezimaldarstellung, und dasselbe gilt für 1/10 mit den Primfaktoren 2 und 5, wobei letzterer nicht mit 2 geteilt wird ;; Dies bedeutet, dass 0,1 nicht genau durch eine endliche Anzahl von Bits nach dem Punktpunkt dargestellt werden kann.

Da 0.1 keine exakte Darstellung hat, versucht eine Funktion, die die Approximation in eine Dezimalzeichenfolge konvertiert, normalerweise, bestimmte Werte zu approximieren, damit sie keine unintuitiven Ergebnisse wie 0.1000000000004121 erhalten.

Da der Gleitkomma in wissenschaftlicher Notation vorliegt, wirkt sich jede Multiplikation mit einer Potenz der Basis nur auf den Exponententeil der Zahl aus. Zum Beispiel 1.231e + 2 * 100 = 1.231e + 4 für die Dezimalschreibweise und ebenso 1.00101010e11 * 100 = 1.00101010e101 in der Binärschreibweise. Wenn ich mit einer Nicht-Potenz der Basis multipliziere, werden auch die signifikanten Ziffern beeinflusst. Zum Beispiel 1.2e1 * 3 = 3.6e1

Abhängig vom verwendeten Algorithmus wird möglicherweise versucht, gemeinsame Dezimalstellen nur anhand der signifikanten Zahlen zu erraten. Sowohl 0,1 als auch 0,4 haben die gleichen signifikanten Zahlen in binärer Form, da ihre Floats im Wesentlichen Kürzungen von (8/5) (2 ^ -4) bzw. (8/5) (2 ^ -6) sind. Wenn der Algorithmus das 8/5-Sigfig-Muster als Dezimalzahl 1,6 identifiziert, funktioniert er mit 0,1, 0,2, 0,4, 0,8 usw. Es kann auch magische Sigfig-Muster für andere Kombinationen geben, z. B. Float 3 geteilt durch Float 10 und andere magische Muster, die statistisch wahrscheinlich durch Division durch 10 gebildet werden.

Im Fall von 3 * 0,1 unterscheiden sich die letzten signifikanten Zahlen wahrscheinlich von der Division eines Floats 3 durch Float 10, was dazu führt, dass der Algorithmus die magische Zahl für die Konstante 0,3 abhängig von seiner Toleranz für Präzisionsverlust nicht erkennt.

Bearbeiten: https://docs.python.org/3.1/tutorial/floatingpoint.html

Interessanterweise gibt es viele verschiedene Dezimalzahlen, die denselben ungefähren binären Bruchteil haben. Beispielsweise werden die Zahlen 0.1 und 0.10000000000000001 und 0.1000000000000000055511151231257827021181583404541015625 alle durch 3602879701896397/2 ** 55 angenähert. ) == x.

Es gibt keine Toleranz für Präzisionsverluste. Wenn float x (0,3) nicht genau gleich float y (0,1 * 3) ist, ist repr (x) nicht genau gleich repr (y).

AkariAkaori
quelle
4
Dies trägt nicht wirklich zu den vorhandenen Antworten bei.
Antti Haapala
1
"Abhängig vom verwendeten Algorithmus wird möglicherweise versucht, gemeinsame Dezimalstellen nur anhand der signifikanten Zahlen zu erraten." <- Dies scheint reine Spekulation zu sein. Andere Antworten haben beschrieben, was Python tatsächlich tut.
Mark Dickinson