PEP 327 – Dezimaldatentyp
- Autor:
- Facundo Batista <facundo at taniquetil.com.ar>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 17-Okt-2003
- Python-Version:
- 2.4
- Post-History:
- 30-Nov-2003, 02-Jan-2004, 29-Jan-2004
Zusammenfassung
Die Idee ist, einen Dezimaldatentyp zu haben, für alle Anwendungen, bei denen Dezimalzahlen benötigt werden, aber binäre Gleitkommazahlen zu ungenau sind.
Der Dezimaldatentyp unterstützt die Standardfunktionen und Operationen von Python und muss der Dezimalarithmetik-ANSI-Norm X3.274-1996 [1] entsprechen.
Dezimal wird Gleitkomma sein (im Gegensatz zu Festkomma) und eine begrenzte Genauigkeit haben (die Genauigkeit ist die Obergrenze für die Anzahl der signifikanten Ziffern in einem Ergebnis). Die Genauigkeit ist jedoch vom Benutzer einstellbar, und eine Vorstellung von signifikanten Nullen am Ende wird unterstützt, so dass auch Festkommaverwendung möglich ist.
Diese Arbeit basiert auf Code und Testfunktionen, die von Eric Price, Aahz und Tim Peters geschrieben wurden. Kurz vor Python 2.4a1 wurde die Referenzimplementierung decimal.py Referenzimplementierung in die Standardbibliothek verschoben; zusammen mit der Dokumentation und der Testsuite war dies die Arbeit von Raymond Hettinger. Ein großer Teil der Erklärungen in diesem PEP stammt aus Cowlishaws Arbeit [2], comp.lang.python und python-dev.
Motivation
Hier werde ich die Gründe darlegen, warum meiner Meinung nach ein Dezimaldatentyp benötigt wird und warum andere numerische Datentypen nicht ausreichen.
Ich wünschte mir einen Gelddatentyp, und nach dem Vorschlag eines Pre-PEPs in comp.lang.python stimmte die Community einem numerischen Datentyp mit dem benötigten arithmetischen Verhalten zu, um dann Geld darauf aufzubauen: Alle Überlegungen zur Anzahl der Nachkommastellen, zur Rundung usw. werden über Geld gehandhabt. Es ist nicht die Aufgabe dieses PEPs, einen Datentyp zu schaffen, der ohne weitere Anstrengungen als Geld verwendet werden kann.
Einer der größten Vorteile der Implementierung eines Standards ist, dass sich jemand bereits um alle kniffligen Fälle gekümmert hat. Und zu einem Standard leitete mich GvR weiter: Mike Cowlishaws Spezifikation für allgemeine Dezimalarithmetik [2]. Dieses Dokument definiert eine allgemeine Dezimalarithmetik. Eine korrekte Implementierung dieser Spezifikation entspricht der in ANSI/IEEE Standard 854-1987 definierten Dezimalarithmetik, abgesehen von einigen geringfügigen Einschränkungen, und bietet auch ungerundete Dezimalarithmetik und ganzzahlige Arithmetik als echte Teilmengen.
Das Problem mit binärem Gleitkomma
In der Dezimalmathematik gibt es viele Zahlen, die nicht mit einer festen Anzahl von Dezimalziffern dargestellt werden können, z. B. 1/3 = 0,3333333333…….
In Basis 2 (der Art und Weise, wie die Standard-Gleitkommazahlen berechnet werden) ist 1/2 = 0,1, 1/4 = 0,01, 1/8 = 0,001 usw. Die Dezimalzahl 0,2 entspricht 2/10, was 1/5 entspricht, und ergibt die binäre Bruchzahl 0,001100110011001... Wie Sie sehen können, besteht das Problem darin, dass einige Dezimalzahlen nicht exakt in binärer Form dargestellt werden können, was zu kleinen Rundungsfehlern führt.
Wir brauchen also einen Dezimaldatentyp, der Dezimalzahlen exakt darstellt. Anstelle eines binären Datentyps benötigen wir einen dezimalen.
Warum Gleitkomma?
Also gehen wir zu Dezimalzahlen, aber warum *Gleitkomma*?
Gleitkommazahlen verwenden eine feste Anzahl von Ziffern (Genauigkeit), um eine Zahl darzustellen, und arbeiten mit einem Exponenten, wenn die Zahl zu groß oder zu klein wird. Zum Beispiel mit einer Genauigkeit von 5
1234 ==> 1234e0
12345 ==> 12345e0
123456 ==> 12346e1
(beachten Sie, dass in der letzten Zeile die Zahl gerundet wurde, um in fünf Ziffern zu passen).
Im Gegensatz dazu haben wir das Beispiel einer `long`-Ganzzahl mit unendlicher Genauigkeit, was bedeutet, dass Sie die Zahl so groß machen können, wie Sie möchten, und Sie niemals Informationen verlieren.
Bei einer Festkommazahl ist die Position des Dezimalpunkts fixiert. Für einen Festkommadatentyp siehe Tim Peters' FixedPoint auf SourceForge [4]. Ich entscheide mich für Gleitkomma, da es einfacher ist, das arithmetische Verhalten des Standards zu implementieren, und Sie dann einen Festkommadatentyp über Decimal implementieren können.
Aber warum können wir keine Gleitkommazahl mit unendlicher Genauigkeit haben? Das ist nicht so einfach, wegen inexakter Divisionen. Z.B.: 1/3 = 0,3333333333333... ad infinitum. In diesem Fall sollten Sie unendlich viele 3en speichern, was zu viel Speicherplatz kostet ;-).
John Roth schlug vor, den Divisionsoperator abzuschaffen und den Benutzer zu zwingen, eine explizite Methode zu verwenden, nur um diese Art von Problemen zu vermeiden. Dies löste heftige Reaktionen in comp.lang.python aus, da jeder die Unterstützung für den `/`-Operator in einem numerischen Datentyp wünscht.
Mit diesem dargelegten Punkt denken Sie vielleicht: "Hey! Können wir nicht einfach die 1 und die 3 als Zähler und Nenner speichern?", was uns zum nächsten Punkt führt.
Warum nicht rational?
Rationale Zahlen werden mit zwei Ganzzahlen gespeichert, dem Zähler und dem Nenner. Dies bedeutet, dass die arithmetischen Operationen nicht direkt ausgeführt werden können (z. B. um zwei rationale Zahlen zu addieren, muss zuerst der gemeinsame Nenner berechnet werden).
Zitat von Alex Martelli
Die Performance-Implikationen der Tatsache, dass die Summe zweier Rationale (die O(M) und O(N) Speicherplatz beanspruchen) ein Rational ergibt, das O(M+N) Speicherplatz beansprucht, ist einfach zu mühsam. Es gibt ausgezeichnete rationale Implementierungen sowohl in reinem Python als auch als Erweiterungen (z.B. gmpy), aber sie werden meiner Meinung nach immer eine "Nischenmarkt" bleiben. Wahrscheinlich wert für ein PEP, nicht aber ohne Decimal – was der richtige Weg ist, um Geldbeträge darzustellen, ein wirklich wichtiger Anwendungsfall in der realen Welt.
Wenn Sie sich für diesen Datentyp interessieren, möchten Sie vielleicht einen Blick auf PEP 239: Hinzufügen eines rationalen Typs zu Python werfen.
Was haben wir also?
Das Ergebnis ist ein Dezimaldatentyp mit begrenzter Genauigkeit und Gleitkomma.
Wird es nützlich sein? Ich kann es nicht besser sagen als Alex Martelli
Python (out of the box) erlaubt Ihnen nicht, binäre Gleitkommazahlen *mit beliebiger von Ihnen angegebener Genauigkeit* zu haben: Sie sind auf das beschränkt, was Ihre Hardware liefert. Dezimal, sei es als Fest- oder Gleitkommazahl verwendet, sollte keine solche Einschränkung haben: Welche begrenzte Genauigkeit Sie auch immer bei der Zahlenerstellung angeben (sofern Ihr Speicher dies zulässt), sollte genauso gut funktionieren. Der Großteil des Aufwands für Programmiervereinfachung kann vor Anwendungsprogrammen versteckt und in einem geeigneten Dezimalarithmetiktyp untergebracht werden. Laut http://speleotrove.com/decimal/ *kann ein einziger Datentyp für dezimale Ganzzahl-, Festkomma- und Gleitkomm-Arithmetik verwendet werden* – und für Geldbetragsarithmetik, die den Anwendungsprogrammierer nicht verrückt macht.
Es gibt verschiedene Verwendungszwecke für einen solchen Datentyp. Wie bereits erwähnt, werde ich ihn als Basis für Geld verwenden. In diesem Fall ist die begrenzte Genauigkeit kein Problem; Zitat von Tim Peters
Eine Genauigkeit von 20 wäre mehr als ausreichend, um die gesamte Weltwirtschaft, bis auf den letzten Penny, seit Anbeginn der Zeit zu berücksichtigen.
Allgemeine Spezifikation für Dezimalarithmetik
Hier werde ich Informationen und Beschreibungen aufnehmen, die Teil der Spezifikation [2] sind (die Struktur der Zahl, der Kontext usw.). Alle in diesem Abschnitt enthaltenen Anforderungen stehen nicht zur Diskussion (abgesehen von Tippfehlern oder anderen Fehlern), da sie im Standard enthalten sind und der PEP nur der Implementierung des Standards dient.
Aufgrund von Urheberrechtsbeschränkungen kann ich hier keine Erklärungen aus der Spezifikation kopieren, daher werde ich versuchen, sie mit meinen eigenen Worten zu erklären. Ich ermutige Sie ausdrücklich, das Original-Spezifikationsdokument [2] für Details zu lesen oder wenn Sie Zweifel haben.
Das arithmetische Modell
Die Spezifikation basiert auf einem Modell der Dezimalarithmetik, wie es von den einschlägigen Standards definiert wird: IEEE 854 [3], ANSI X3-274 [1] und der vorgeschlagenen Überarbeitung [5] von IEEE 754 [6].
Das Modell hat drei Komponenten
- Zahlen: nur die Werte, die die Operation als Eingabe oder Ausgabe verwendet.
- Operationen: Addition, Multiplikation usw.
- Kontext: eine Reihe von Parametern und Regeln, die der Benutzer auswählen kann und die die Ergebnisse von Operationen steuern (z. B. die zu verwendende Genauigkeit).
Zahlen
Zahlen können endlich oder spezielle Werte sein. Erstere können exakt dargestellt werden. Letztere sind Unendlichkeiten und undefiniert (wie 0/0).
Endliche Zahlen werden durch drei Parameter definiert
- Vorzeichen: 0 (positiv) oder 1 (negativ).
- Koeffizient: eine nicht-negative Ganzzahl.
- Exponent: eine vorzeichenbehaftete ganze Zahl, die Zehnerpotenz des Koeffizientenmultiplikators.
Der numerische Wert einer endlichen Zahl ist gegeben durch
(-1)**sign * coefficient * 10**exponent
Spezielle Werte werden wie folgt benannt
- Unendlich: ein Wert, der unendlich groß ist. Kann positiv oder negativ sein.
- Stille NaN ("qNaN"): repräsentiert undefinierte Ergebnisse (*Not a Number*). Verursacht keine "Ungültige Operation"-Bedingung. Das Vorzeichen einer NaN hat keine Bedeutung.
- Signalierende NaN ("sNaN"): ebenfalls *Not a Number*, verursacht aber eine "Ungültige Operation"-Bedingung, wenn sie in irgendeiner Operation verwendet wird.
Kontext
Der Kontext ist eine Reihe von Parametern und Regeln, die der Benutzer auswählen kann und die die Ergebnisse von Operationen steuern (z. B. die zu verwendende Genauigkeit).
Der Kontext erhält diesen Namen, weil er die Dezimalzahlen umschließt, wobei Teile des Kontexts als Eingabe und Ausgabe von Operationen dienen. Es liegt an der Anwendung, mit einem oder mehreren Kontexten zu arbeiten, aber definitiv ist nicht die Idee, einen Kontext pro Dezimalzahl zu haben. Eine typische Verwendung wäre beispielsweise, die Genauigkeit des Kontexts zu Beginn eines Programms auf 20 Ziffern einzustellen und den Kontext nie wieder explizit zu verwenden.
Diese Definitionen beeinträchtigen nicht die interne Speicherung der Dezimalzahlen, sondern nur die Art und Weise, wie die arithmetischen Operationen durchgeführt werden.
Der Kontext wird hauptsächlich durch die folgenden Parameter definiert (siehe Kontextattribute für alle Kontextattribute)
- Genauigkeit: Die maximale Anzahl signifikanter Ziffern, die sich aus einer arithmetischen Operation ergeben können (Ganzzahl > 0). Es gibt keine Obergrenze für diesen Wert.
- Rundung: Der Name des Algorithmus, der verwendet werden soll, wenn eine Rundung erforderlich ist, einer von "round-down", "round-half-up", "round-half-even", "round-ceiling", "round-floor", "round-half-down" und "round-up". Siehe Rundungsalgorithmen unten.
- Flags und Trap-Enabler: Ausnahmezustände sind in Signale gruppiert, die individuell steuerbar sind, wobei jedes aus einem Flag (boolesch, gesetzt, wenn das Signal auftritt) und einem Trap-Enabler (ein boolescher Wert, der das Verhalten steuert) besteht. Die Signale sind: "clamped", "division-by-zero", "inexact", "invalid-operation", "overflow", "rounded", "subnormal" und "underflow".
Standardkontexte
Die Spezifikation definiert zwei Standardkontexte, die für den Benutzer leicht auswählbar sein sollten.
Grundlegender Standardkontext
- Flags: alle auf 0 gesetzt
- Trap-Enabler: inexact, rounded und subnormal sind auf 0 gesetzt; alle anderen sind auf 1 gesetzt
- Genauigkeit: ist auf 9 gesetzt
- Rundung: ist auf round-half-up gesetzt
Erweiterter Standardkontext
- Flags: alle auf 0 gesetzt
- Trap-Enabler: alle auf 0 gesetzt
- Genauigkeit: ist auf 9 gesetzt
- Rundung: ist auf round-half-even gesetzt
Ausnahmezustände
Die folgende Tabelle listet die außergewöhnlichen Bedingungen auf, die während der arithmetischen Operationen auftreten können, das entsprechende Signal und das definierte Ergebnis. Details finden Sie in der Spezifikation [2].
| Bedingung | Signal | Ergebnis |
|---|---|---|
| Geklemmt | clamped | siehe Spec [2] |
| Division durch Null | division-by-zero | [Vorzeichen,inf] |
| Ungenau | inexact | unverändert |
| Ungültige Operation | invalid-operation | [0,qNaN] (oder [s,qNaN] oder [s,qNaN,d], wenn die Ursache eine signalierende NaN ist) |
| Überlauf | overflow | abhängig vom Rundungsmodus |
| Gerundet | rounded | unverändert |
| Subnormal | subnormal | unverändert |
| Unterlauf | underflow | siehe Spec [2] |
Hinweis: Wenn der Standard von "unzureichendem Speicher" spricht, wird diese Implementierung, solange es sich um eine implementierungsspezifische Verhaltensweise handelt, bei der nicht genügend Speicher vorhanden ist, um die Interna der Zahl zu speichern, eine MemoryError auslösen.
Bezüglich Überlauf und Unterlauf gab es eine lange Diskussion in python-dev über künstliche Grenzen. Der allgemeine Konsens ist, die künstlichen Grenzen nur beizubehalten, wenn wichtige Gründe dafür sprechen. Tim Peters gibt uns drei
...das Eliminieren von Grenzen für Exponenten bedeutet effektiv, dass Überlauf (und Unterlauf) niemals passieren können. Aber Überlauf *ist* ein wertvolles Sicherheitsnetz im realen FP-Gebrauch, wie ein Kanarienvogel in der Kohlemine, der frühzeitig Warnsignale gibt, wenn ein Programm verrückt spielt.Praktisch alle Implementierungen von 854 verwenden (und wie der IBM-Standard sogar vorschlägt) "verbotene" Exponentenwerte, um nicht-endliche Zahlen (Unendlichkeiten und NaNs) zu kodieren. Ein begrenzter Exponent kann dies mit praktisch keinen zusätzlichen Speicherkosten tun. Wenn der Exponent unbegrenzt ist, müssen stattdessen zusätzliche Bits verwendet werden. Diese Kosten bleiben verborgen, bis zeit- und speichereffizientere Implementierungen versucht werden.
So groß er auch ist, der IBM-Standard ist ein winziger Anfang, um eine vollständige numerische Einrichtung bereitzustellen. Keine Begrenzung der Exponentengröße wird die Implementierung von z. B. Dezimal-Sinus und -Kosinus enorm verkomplizieren (es gibt dann keine a priori Grenze, wie viele Stellen von pi effektiv bekannt sein müssen, um die Argumentreduktion durchzuführen).
Edward Loper gibt uns ein Beispiel dafür, wann die Grenzen überschritten werden sollen: Wahrscheinlichkeiten.
Das heißt, Robert Brewer und Andrew Lentvorski möchten, dass die Grenzen für die Benutzer leicht modifizierbar sind. Das ist tatsächlich gut möglich
>>> d1 = Decimal("1e999999999") # at the exponent limit
>>> d1
Decimal("1E+999999999")
>>> d1 * 10 # exceed the limit, got infinity
Traceback (most recent call last):
File "<pyshell#3>", line 1, in ?
d1 * 10
...
...
Overflow: above Emax
>>> getcontext().Emax = 1000000000 # increase the limit
>>> d1 * 10 # does not exceed any more
Decimal("1.0E+1000000000")
>>> d1 * 100 # exceed again
Traceback (most recent call last):
File "<pyshell#3>", line 1, in ?
d1 * 100
...
...
Overflow: above Emax
Rundungsalgorithmen
`round-down`: Die verworfenen Ziffern werden ignoriert; das Ergebnis bleibt unverändert (Rundung gegen 0, Trunkierung)
1.123 --> 1.12
1.128 --> 1.12
1.125 --> 1.12
1.135 --> 1.13
`round-half-up`: Wenn die verworfenen Ziffern größer oder gleich der Hälfte (0,5) sind, wird das Ergebnis um 1 erhöht; andernfalls werden die verworfenen Ziffern ignoriert
1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.13
1.135 --> 1.14
`round-half-even`: Wenn die verworfenen Ziffern größer als die Hälfte (0,5) darstellen, wird der Koeffizient des Ergebnisses um 1 erhöht; wenn sie weniger als die Hälfte darstellen, wird das Ergebnis nicht angepasst; andernfalls bleibt das Ergebnis unverändert, wenn seine letzte Ziffer gerade ist, oder es wird um 1 erhöht, wenn seine letzte Ziffer ungerade ist (um eine gerade Ziffer zu bilden)
1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.12
1.135 --> 1.14
`round-ceiling`: Wenn alle verworfenen Ziffern Null sind oder wenn das Vorzeichen negativ ist, bleibt das Ergebnis unverändert; andernfalls wird das Ergebnis um 1 erhöht (Rundung gegen unendlich)
1.123 --> 1.13
1.128 --> 1.13
-1.123 --> -1.12
-1.128 --> -1.12
`round-floor`: Wenn alle verworfenen Ziffern Null sind oder wenn das Vorzeichen positiv ist, bleibt das Ergebnis unverändert; andernfalls wird der absolute Wert des Ergebnisses um 1 erhöht (Rundung gegen negativ unendlich)
1.123 --> 1.12
1.128 --> 1.12
-1.123 --> -1.13
-1.128 --> -1.13
`round-half-down`: Wenn die verworfenen Ziffern größer als die Hälfte (0,5) darstellen, wird das Ergebnis um 1 erhöht; andernfalls werden die verworfenen Ziffern ignoriert
1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.12
1.135 --> 1.13
`round-up`: Wenn alle verworfenen Ziffern Null sind, bleibt das Ergebnis unverändert, andernfalls wird das Ergebnis um 1 erhöht (Rundung vom 0 weg)
1.123 --> 1.13
1.128 --> 1.13
1.125 --> 1.13
1.135 --> 1.14
Begründung
Ich muss die Anforderungen in zwei Abschnitte trennen. Der erste ist die Einhaltung des ANSI-Standards. Alle Anforderungen dafür sind in Mike Cowlishaws Werk [2] spezifiziert. Er hat auch eine **sehr große** Suite von Testfällen bereitgestellt.
Der zweite Abschnitt der Anforderungen (Unterstützung von Standard-Python-Funktionen, Benutzerfreundlichkeit usw.) ist von hier an detailliert, wo ich alle getroffenen Entscheidungen und deren Gründe sowie alle noch diskutierten Themen aufnehmen werde.
Explizite Konstruktion
Die explizite Konstruktion wird vom Kontext nicht beeinflusst (es gibt keine Rundung, keine Präzisionsgrenzen usw.), da der Kontext nur die Ergebnisse von Operationen beeinflusst. Die einzige Ausnahme ist, wenn Sie aus dem Kontext erstellen.
Aus int oder long
Es gibt keinen Verlust und keine Notwendigkeit, weitere Informationen anzugeben
Decimal(35)
Decimal(-124)
Aus String
Strings, die Python-Dezimalzahl-Literale und Python-Float-Literale enthalten, werden unterstützt. Bei dieser Umwandlung geht keine Information verloren, da der String direkt in Decimal umgewandelt wird (es gibt keine Zwischenumwandlung über Float).
Decimal("-12")
Decimal("23.2e-7")
Außerdem können Sie auf diese Weise alle speziellen Werte (Unendlichkeit und Nicht-eine-Zahl) konstruieren.
Decimal("Inf")
Decimal("NaN")
Aus Float
Die anfängliche Diskussion zu diesem Punkt drehte sich darum, was passieren soll, wenn man einen Gleitkommawert an den Konstruktor übergibt.
`Decimal(1.1)` == `Decimal('1.1')``Decimal(1.1)` == `Decimal('110000000000000008881784197001252...e-51')`- eine Ausnahme wird ausgelöst
Mehrere Personen behaupteten, dass (1) die bessere Option sei, da dies das ist, was man erwartet, wenn man `Decimal(1.1)` schreibt. Und laut John Roth ist es einfach zu implementieren.
Es ist überhaupt nicht schwierig herauszufinden, wo die tatsächliche Zahl endet und wo der Fuzz beginnt. Sie können es visuell tun, und die Algorithmen dafür sind recht bekannt.
Aber wenn ich *wirklich* möchte, dass meine Zahl `Decimal('110000000000000008881784197001252...e-51')` ist, warum kann ich dann nicht `Decimal(1.1)` schreiben? Warum sollte ich erwarten, dass Decimal sie "rundet"? Denken Sie daran, dass `1.1` binäres Gleitkomma *ist*, daher kann ich das Ergebnis vorhersagen. Es ist für einen Anfänger nicht intuitiv, aber so ist es.
Wie auch immer, Paul Moore zeigte, dass (1) nicht funktionieren kann, weil
(1) says D(1.1) == D('1.1')
but 1.1 == 1.1000000000000001
so D(1.1) == D(1.1000000000000001)
together: D(1.1000000000000001) == D('1.1')
was falsch ist, denn wenn ich `Decimal('1.1')` schreibe, ist es exakt, nicht `D(1.1000000000000001)`. Er schlug auch vor, eine explizite Konvertierung zu Float zu haben. bokr sagt, dass man die Genauigkeit im Konstruktor angeben muss, und mwilson stimmte zu
d = Decimal (1.1, 1) # take float value to 1 decimal place
d = Decimal (1.1) # gets `places` from pre-set context
Aber Alex Martelli sagt, dass
Die Konstruktion mit einer bestimmten Genauigkeit wäre in Ordnung. Daher glaube ich, dass "Konstruktion aus Float mit einer Standardgenauigkeit" ein erhebliches Risiko birgt, naive Benutzer in die Irre zu führen.
Daher ist die akzeptierte Lösung über c.l.p, dass Sie Decimal nicht mit einem Float aufrufen können. Stattdessen müssen Sie eine Methode verwenden: Decimal.from_float(). Die Syntax
Decimal.from_float(floatNumber, [decimal_places])
wobei `floatNumber` die Float-Zahl ist, aus der die Konstruktion erfolgt, und `decimal_places` die Anzahl der Ziffern nach dem Dezimalpunkt ist, auf die eine round-half-up-Rundung angewendet wird, falls vorhanden. So können Sie zum Beispiel tun
Decimal.from_float(1.1, 2): The same as doing Decimal('1.1').
Decimal.from_float(1.1, 16): The same as doing Decimal('1.1000000000000001').
Decimal.from_float(1.1): The same as doing Decimal('1100000000000000088817841970012523233890533447265625e-51').
Aufgrund späterer Diskussionen wurde beschlossen, from_float() aus der API für Py2.4 wegzulassen. Mehrere Ideen trugen zum Denkprozess bei
- Interaktionen zwischen Dezimal- und Binär-Gleitkommazahlen zwingen den Benutzer, sich mit kniffligen Problemen der Darstellung und Rundung auseinanderzusetzen. Die Vermeidung dieser Probleme ist einer der Hauptgründe für die Existenz des Moduls.
- Die erste Veröffentlichung des Moduls sollte sich auf das konzentrieren, was sicher, minimal und essentiell ist.
- Obwohl theoretisch nett, fehlen reale Anwendungsfälle für Interaktionen zwischen Floats und Dezimalzahlen. Java enthielt Float/Dezimal-Konvertierungen, um einen obskuren Fall zu behandeln, bei dem Berechnungen am besten in Dezimalform durchgeführt werden, obwohl eine Legacy-Datenstruktur die Ein- und Ausgaben in binärer Gleitkommadarstellung erfordert.
- Wenn der Bedarf entsteht, können Benutzer String-Darstellungen als Zwischenformat verwenden. Der Vorteil dieses Ansatzes ist, dass er Annahmen über Genauigkeit und Darstellung explizit macht (kein Rätseln, was unter der Haube vor sich geht).
- Die Java-Dokumentation für BigDecimal(double val) spiegelte ihre Erfahrungen mit dem Konstruktor wider
The results of this constructor can be somewhat unpredictable and its use is generally not recommended.
Aus Tupeln
Aahz schlug vor, aus Tupeln zu konstruieren: Es ist einfacher, den Rundtrip von `eval()` zu implementieren, und "jemand, der numerische Werte hat, die ein Decimal darstellen, muss sie nicht in einen String umwandeln."
Die Struktur wird ein Tupel aus drei Elementen sein: Vorzeichen, Zahl und Exponent. Das Vorzeichen ist 1 oder 0, die Zahl ist ein Tupel von Dezimalziffern und der Exponent ist ein vorzeichenbehafteter int oder long.
Decimal((1, (3, 2, 2, 5), -2)) # for -32.25
Natürlich können Sie auf diese Weise alle speziellen Werte konstruieren.
Decimal( (0, (0,), 'F') ) # for Infinity
Decimal( (0, (0,), 'n') ) # for Not a Number
Aus Decimal
Kein Rätsel hier, nur eine Kopie.
Syntax für alle Fälle
Decimal(value1)
Decimal.from_float(value2, [decimal_places])
wobei `value1` int, long, string, 3-Tupel oder Decimal sein kann, `value2` nur float sein kann, und `decimal_places` eine optionale nicht-negative Ganzzahl ist.
Erstellung aus Kontext
Dieser Punkt entstand in python-dev aus zwei parallelen Quellen. Ka-Ping Yee schlägt vor, den Kontext als Argument bei der Instanzerstellung zu übergeben (er möchte, dass der von ihm übergebene Kontext nur zur Erstellungszeit verwendet wird: "Er wäre nicht persistent"). Tony Meyer bittet from_string, den Kontext zu berücksichtigen, wenn er einen Parameter "honour_context" mit dem Wert True erhält. (Das gefällt mir nicht, da die Dokumentation besagt, dass der Kontext berücksichtigt werden soll, und ich möchte nicht, dass die Methode die Spezifikation bezüglich des Werts eines Arguments einhält.)
Tim Peters gibt uns einen Grund, eine Erstellung zu haben, die den Kontext nutzt.
Bei der allgemeinen Zahlenberechnung können Literale mit hoher Genauigkeit angegeben werden, aber diese Genauigkeit ist nicht kostenlos und *normalerweise* nicht erforderlich.
Casey Duncan möchte eine andere Methode verwenden, keine boolesche Argument.
Ich halte boolesche Argumente für ein allgemeines Anti-Muster, insbesondere da wir Klassenmethoden haben. Warum nicht einen alternativen Konstruktor wie Decimal.rounded_to_context("3.14159265") verwenden.
Bei der Entscheidung über die Syntax dafür kam Tim auf eine bessere Idee: Er schlägt vor, keine Methode in Decimal zu haben, um mit einem anderen Kontext zu erstellen, sondern stattdessen eine Methode in Context zu haben, um eine Decimal-Instanz zu erstellen. Im Grunde, anstatt
D.using_context(number, context)
wird es sein
context.create_decimal(number)
Von Tim
Während alle Operationen in der Spezifikation außer den beiden To-String-Operationen den Kontext verwenden, unterstützen keine Operationen in der Spezifikation einen optionalen lokalen Kontext. Dass der Decimal()-Konstruktor den Kontext standardmäßig ignoriert, ist eine Erweiterung der Spezifikation. Wir müssen eine kontextberücksichtigende From-String-Operation bereitstellen, um die Spezifikation zu erfüllen. Ich rate von jedem Konzept eines "lokalen Kontexts" bei jeder Operation ab – es verkompliziert das Modell und ist nicht notwendig.
Wir haben uns also entschieden, eine Kontextmethode zu verwenden, um ein Decimal zu erstellen, das (nur zur Erstellung) diesen Kontext verwendet. Für weitere Operationen wird es den Kontext des Threads verwenden. Aber mit welchem Namen?
Tim Peters schlägt drei Methoden vor, um aus verschiedenen Quellen zu erstellen (from_string, from_int, from_float). Ich schlug vor, eine Methode, `create_decimal()`, zu verwenden, ohne sich um den Datentyp zu kümmern. Michael Chermside: "Der Name passt einfach zu meinem Gehirn. Dass es den Kontext verwendet, ist offensichtlich, da es eine Kontextmethode ist."
Die Community stimmte dem zu. Ich denke, das ist in Ordnung, weil ein Anfänger die Erstellungsmethode aus Context nicht verwenden wird (die separate Methode in Decimal, um aus Float zu konstruieren, dient nur dazu, Anfänger von binären Gleitkomma-Problemen fernzuhalten).
Kurz gesagt, wenn Sie eine Decimal-Instanz mit einem bestimmten Kontext erstellen möchten (der nur zur Erstellungszeit und nicht weiter verwendet wird), müssen Sie eine Methode dieses Kontexts verwenden.
# n is any datatype accepted in Decimal(n) plus float
mycontext.create_decimal(n)
Beispiel
>>> # create a standard decimal instance
>>> Decimal("11.2233445566778899")
Decimal("11.2233445566778899")
>>>
>>> # create a decimal instance using the thread context
>>> thread_context = getcontext()
>>> thread_context.prec
28
>>> thread_context.create_decimal("11.2233445566778899")
Decimal("11.2233445566778899")
>>>
>>> # create a decimal instance using other context
>>> other_context = thread_context.copy()
>>> other_context.prec = 4
>>> other_context.create_decimal("11.2233445566778899")
Decimal("11.22")
Implizite Konstruktion
Da die implizite Konstruktion die Folge einer Operation ist, wird sie wie in jedem Punkt detailliert beschrieben vom Kontext beeinflusst.
John Roth schlug vor, dass "Der andere Typ auf die gleiche Weise behandelt werden sollte, wie der decimal()-Konstruktor es tun würde". Aber Alex Martelli denkt, dass
dieser vollständige Bruch mit der Python-Tradition wäre ein furchtbarer Fehler. 23+"43" wird NICHT auf die gleiche Weise behandelt wie 23+int("45"), und das ist auch gut so. Es ist eine völlig andere Sache für einen Benutzer, EXPLICIT zu signalisieren, dass er eine Konstruktion (Konvertierung) möchte, und nur zufällig zwei Objekte zu summieren, von denen eines versehentlich eine Zeichenkette sein könnte.
Daher definiere ich hier das Verhalten für jeden Datentyp neu.
Aus int oder long
Eine int- oder long-Ganzzahl wird wie eine explizit aus Decimal(str(x)) im aktuellen Kontext konstruierte Dezimalzahl behandelt (was bedeutet, dass die To-String-Regeln für die Rundung angewendet und die entsprechenden Flags gesetzt werden). Dies garantiert, dass Ausdrücke wie `Decimal('1234567')` + `13579` dem Gedankenmodell von `Decimal('1234567')` + `Decimal('13579')` entsprechen. Dieses Modell funktioniert, da alle Ganzzahlen als Zeichenketten ohne Darstellungsfehler darstellbar sind.
Aus String
Alle sind sich einig, hier eine Ausnahme auszulösen.
Aus Float
Aahz lehnt die Interaktion mit Float vehement ab und schlägt eine explizite Konvertierung vor.
Das Problem ist, dass Decimal eine höhere Genauigkeit, Präzision und einen größeren Bereich hat als Float.
Das Beispiel des gültigen Python-Ausdrucks `35` + `1.1` scheint darauf hinzudeuten, dass `Decimal(35)` + `1.1` ebenfalls gültig sein sollte. Ein genauerer Blick zeigt jedoch, dass es nur die Machbarkeit von Ganzzahl-zu-Gleitkomma-Konvertierungen demonstriert. Daher ist das korrekte Analogon für Dezimal-Gleitkomma `35` + `Decimal(1.1)`. Beide Koerzionen, Int-zu-Float und Int-zu-Decimal, können ohne Darstellungsfehler erfolgen.
Die Frage, wie zwischen Binär- und Dezimal-Gleitkomma koerziert wird, ist komplexer. Ich schlug vor, die Interaktion mit Float zu erlauben und eine exakte Konvertierung durchzuführen und ValueError auszulösen, wenn die Genauigkeit im aktuellen Kontext überschritten wird (dies ist vielleicht zu knifflig, denn zum Beispiel mit einer Genauigkeit von 9 ist `Decimal(35)` + `1.2` OK, aber `Decimal(35)` + `1.1` löst einen Fehler aus).
Dies erwies sich als zu knifflig. So knifflig, dass c.l.p vereinbarte, in diesem Fall TypeError auszulösen: Man könnte Decimal und Float nicht mischen.
Aus Decimal
Hier gibt es kein Problem.
Verwendung von Kontext
Im letzten PEP-Entwurf habe ich gesagt: „Der Kontext muss allgegenwärtig sein, was bedeutet, dass Änderungen daran alle aktuellen und zukünftigen Dezimalinstanzen beeinflussen.“ Ich lag falsch. Als Antwort darauf sagte John Roth:
Der Kontext sollte für den jeweiligen Verwendungszweck auswählbar sein. Das heißt, es sollte möglich sein, mehrere verschiedene Kontexte gleichzeitig in einer Anwendung zu verwenden.
In comp.lang.python erklärte Aahz, dass die Idee darin besteht, einen „Kontext pro Thread“ zu haben. So gehören alle Instanzen eines Threads zu einem Kontext, und Sie können einen Kontext in Thread A (und das Verhalten der Instanzen dieses Threads) ändern, ohne etwas in Thread B zu ändern.
Auch und wieder, um mich zu korrigieren, sagte er:
Der Kontext gilt nur für Operationen, nicht für Dezimalinstanzen; Das Ändern des Kontexts hat keine Auswirkungen auf vorhandene Instanzen, wenn keine Operationen auf ihnen ausgeführt werden.
Tim Peters argumentierte über Sonderfälle, bei denen Operationen mit anderen Regeln als denen des aktuellen Kontexts ausgeführt werden müssen, und sagte, dass der Kontext die Operationen als Methoden haben wird. Auf diese Weise kann der Benutzer „beliebige private Kontextobjekte erstellen, die er benötigt, und die Arithmetik als explizite Methodenaufrufe auf seinen privaten Kontextobjekten buchstabieren, so dass der standardmäßige Thread-Kontext nicht konsultiert oder geändert wird“.
Benutzerfreundlichkeit in Python
- Decimal sollte die grundlegenden arithmetischen (
+, -, *, /, //, **, %, divmod) und Vergleichsoperatoren (==, !=, <, >, <=, >=, cmp) in den folgenden Fällen unterstützen (prüfen Sie unter Implizite Konstruktion, welche Typen OtherType sein könnten und was in jedem Fall passiert)- Decimal op Decimal
- Decimal op otherType
- otherType op Decimal
- Decimal op= Decimal
- Decimal op= otherType
- Decimal sollte unäre Operatoren unterstützen (
-, +, abs). - repr() sollte Round-Trip ermöglichen, was bedeutet,
m = Decimal(...) m == eval(repr(m))
- Decimal sollte unveränderlich sein.
- Decimal sollte die integrierten Methoden unterstützen
- min, max
- float, int, long
- str, repr
- hash
- bool (0 ist falsch, sonst wahr)
Es gab einige Diskussionen in python-dev über das Verhalten von hash(). Die Community ist sich einig, dass, wenn die Werte gleich sind, auch die Hashes dieser Werte gleich sein sollten. Während also Decimal(25) == 25 True ist, sollte hash(Decimal(25)) gleich hash(25) sein.
Das Detail ist, dass Sie Decimal NICHT mit Floats oder Strings vergleichen können, also sollten wir uns keine Sorgen machen, dass diese die gleichen Hashes ergeben. Kurz gesagt:
hash(n) == hash(Decimal(n)) # Only if n is int, long, or Decimal
Bezüglich des Verhaltens von str() und repr() schlägt Ka-Ping Yee vor, dass repr() das gleiche Verhalten wie str() haben sollte, und Tim Peters schlägt vor, dass str() wie die to-scientific-string-Operation aus der Spezifikation funktionieren sollte.
Dies ist möglich, da (von Aahz): „Die Zeichenfolgendarstellung enthält bereits alle notwendigen Informationen, um ein Decimal-Objekt zu rekonstruieren“.
Und es entspricht auch der Spezifikation; Tim Peters
Es gibt keine Anforderung, eine Methode namens "to_sci_string" zu haben, die einzige Anforderung ist, dass *irgendeine* Möglichkeit, die Funktionalität von to-sci-string zu realisieren, bereitgestellt wird. Die Bedeutung von to-sci-string ist genau durch den Standard spezifiziert und ist eine gute Wahl sowohl für str(Decimal) als auch für repr(Decimal).
Dokumentation
Dieser Abschnitt erklärt alle öffentlichen Methoden und Attribute von Decimal und Context.
Dezimalattribute
Decimal hat keine öffentlichen Attribute. Die internen Informationen werden in Slots gespeichert und sollten nicht von Endbenutzern aufgerufen werden.
Dezimalmethoden
Im Folgenden sind die Konvertierungs- und arithmetischen Operationen aufgeführt, die in der Spezifikation definiert sind, und wie diese Funktionalität mit der aktuellen Implementierung erreicht werden kann.
- to-scientific-string: Verwenden Sie die integrierte Funktion
str()>>> d = Decimal('123456789012.345') >>> str(d) '1.23456789E+11'
- to-engineering-string: Verwenden Sie die Methode
to_eng_string()>>> d = Decimal('123456789012.345') >>> d.to_eng_string() '123.456789E+9'
- to-number: Verwenden Sie die Kontextmethode
create_decimal(). Der Standardkonstruktor oder derfrom_float()Konstruktor können nicht verwendet werden, da diese den Kontext nicht verwenden (wie in der Spezifikation für diese Konvertierung angegeben). - abs: Verwenden Sie die integrierte Funktion
abs()>>> d = Decimal('-15.67') >>> abs(d) Decimal('15.67')
- add: Verwenden Sie den Operator
+>>> d = Decimal('15.6') >>> d + 8 Decimal('23.6')
- subtract: Verwenden Sie den Operator
->>> d = Decimal('15.6') >>> d - 8 Decimal('7.6')
- compare: Verwenden Sie die Methode
compare(). Diese Methode (und nicht die integrierte Funktion cmp()) sollte nur bei der Behandlung von *Spezialwerten* verwendet werden.>>> d = Decimal('-15.67') >>> nan = Decimal('NaN') >>> d.compare(23) '-1' >>> d.compare(nan) 'NaN' >>> cmp(d, 23) -1 >>> cmp(d, nan) 1
- divide: Verwenden Sie den Operator
/>>> d = Decimal('-15.67') >>> d / 2 Decimal('-7.835')
- divide-integer: Verwenden Sie den Operator
//>>> d = Decimal('-15.67') >>> d // 2 Decimal('-7')
- max: Verwenden Sie die Methode
max(). Verwenden Sie diese Methode (und nicht die integrierte Funktion max()) nur bei der Behandlung von *Spezialwerten*.>>> d = Decimal('15') >>> nan = Decimal('NaN') >>> d.max(8) Decimal('15') >>> d.max(nan) Decimal('NaN')
- min: Verwenden Sie die Methode
min(). Verwenden Sie diese Methode (und nicht die integrierte Funktion min()) nur bei der Behandlung von *Spezialwerten*.>>> d = Decimal('15') >>> nan = Decimal('NaN') >>> d.min(8) Decimal('8') >>> d.min(nan) Decimal('NaN')
- minus: Verwenden Sie den unären Operator
->>> d = Decimal('-15.67') >>> -d Decimal('15.67')
- plus: Verwenden Sie den unären Operator
+>>> d = Decimal('-15.67') >>> +d Decimal('-15.67')
- multiply: Verwenden Sie den Operator
*>>> d = Decimal('5.7') >>> d * 3 Decimal('17.1')
- normalize: Verwenden Sie die Methode
normalize()>>> d = Decimal('123.45000') >>> d.normalize() Decimal('123.45') >>> d = Decimal('120.00') >>> d.normalize() Decimal('1.2E+2')
- quantize: Verwenden Sie die Methode
quantize()>>> d = Decimal('2.17') >>> d.quantize(Decimal('0.001')) Decimal('2.170') >>> d.quantize(Decimal('0.1')) Decimal('2.2')
- remainder: Verwenden Sie den Operator
%>>> d = Decimal('10') >>> d % 3 Decimal('1') >>> d % 6 Decimal('4')
- remainder-near: Verwenden Sie die Methode
remainder_near()>>> d = Decimal('10') >>> d.remainder_near(3) Decimal('1') >>> d.remainder_near(6) Decimal('-2')
- round-to-integral-value: Verwenden Sie die Methode
to_integral()>>> d = Decimal('-123.456') >>> d.to_integral() Decimal('-123')
- same-quantum: Verwenden Sie die Methode
same_quantum()>>> d = Decimal('123.456') >>> d.same_quantum(Decimal('0.001')) True >>> d.same_quantum(Decimal('0.01')) False
- square-root: Verwenden Sie die Methode
sqrt()>>> d = Decimal('123.456') >>> d.sqrt() Decimal('11.1110756')
- power: Verwenden Sie den Operator
**>>> d = Decimal('12.56') >>> d ** 2 Decimal('157.7536')
Im Folgenden sind weitere Methoden aufgeführt und warum sie existieren:
adjusted(): Gibt den angepassten Exponenten zurück. Dieses Konzept ist in der Spezifikation definiert: Der angepasste Exponent ist der Wert des Exponenten einer Zahl, wenn diese Zahl so ausgedrückt wird, als ob sie in wissenschaftlicher Notation mit einer Ziffer vor einem Dezimalpunkt stünde.>>> d = Decimal('12.56') >>> d.adjusted() 1
from_float(): Klassenmethode zum Erstellen von Instanzen aus Float-Datentypen.>>> d = Decimal.from_float(12.35) >>> d Decimal('12.3500000')
as_tuple(): Zeigt die interne Struktur des Dezimaltyps, das Triple-Tupel. Diese Methode ist nicht von der Spezifikation gefordert, aber Tim Peters hat sie vorgeschlagen und die Community stimmte ihrer Aufnahme zu (sie ist nützlich für Entwicklung und Debugging).>>> d = Decimal('123.4') >>> d.as_tuple() (0, (1, 2, 3, 4), -1) >>> d = Decimal('-2.34e5') >>> d.as_tuple() (1, (2, 3, 4), 3)
Kontextattribute
Dies sind die Attribute, die geändert werden können, um den Kontext zu modifizieren.
prec(int): die Präzision>>> c.prec 9
rounding(str): Rundungstyp (wie gerundet wird)>>> c.rounding 'half_even'
trap_enablers(dict): Wenn trap_enablers[exception] = 1 ist, wird eine Ausnahme ausgelöst, wenn sie verursacht wird.>>> c.trap_enablers[Underflow] 0 >>> c.trap_enablers[Clamped] 0
flags(dict): Wenn eine Ausnahme verursacht wird, wird flags[exception] inkrementiert (unabhängig davon, ob der trap_enabler gesetzt ist). Sollte vom Benutzer der Decimal-Instanz zurückgesetzt werden.>>> c.flags[Underflow] 0 >>> c.flags[Clamped] 0
Emin(int): minimaler Exponent>>> c.Emin -999999999
Emax(int): maximaler Exponent>>> c.Emax 999999999
capitals(int): boolesches Flag zur Verwendung von 'E' (True/1) oder 'e' (False/0) in der Zeichenfolge (z.B. '1.32e+2' oder '1.32E+2').>>> c.capitals 1
Kontextmethoden
Die folgenden Methoden entsprechen der Decimal-Funktionalität aus der Spezifikation. Beachten Sie, dass die Operationen, die über einen bestimmten Kontext aufgerufen werden, diesen Kontext und nicht den Thread-Kontext verwenden.
Um diese Methoden zu verwenden, beachten Sie, dass sich die Syntax ändert, wenn der Operator binär oder unär ist, zum Beispiel:
>>> mycontext.abs(Decimal('-2'))
'2'
>>> mycontext.multiply(Decimal('2.3'), 5)
'11.5'
Die folgenden sind also die Spec-Operationen und -Konvertierungen und wie sie über einen Kontext erreicht werden (wobei d eine Dezimalinstanz ist und n eine Zahl, die in einer impliziten Konstruktion verwendet werden kann)
- to-scientific-string:
to_sci_string(d) - to-engineering-string:
to_eng_string(d) - to-number:
create_decimal(number), siehe Explizite Konstruktion fürnumber. - abs:
abs(d) - add:
add(d, n) - subtract:
subtract(d, n) - compare:
compare(d, n) - divide:
divide(d, n) - divide-integer:
divide_int(d, n) - max:
max(d, n) - min:
min(d, n) - minus:
minus(d) - plus:
plus(d) - multiply:
multiply(d, n) - normalize:
normalize(d) - quantize:
quantize(d, d) - remainder:
remainder(d) - remainder-near:
remainder_near(d) - round-to-integral-value:
to_integral(d) - same-quantum:
same_quantum(d, d) - square-root:
sqrt(d) - power:
power(d, n)
Die Methode divmod(d, n) unterstützt Dezimalfunktionalität über den Kontext.
Dies sind Methoden, die nützliche Informationen aus dem Kontext zurückgeben:
Etiny(): Minimaler Exponent unter Berücksichtigung der Präzision.>>> c.Emin -999999999 >>> c.Etiny() -1000000007
Etop(): Maximaler Exponent unter Berücksichtigung der Präzision.>>> c.Emax 999999999 >>> c.Etop() 999999991
copy(): Gibt eine Kopie des Kontexts zurück.
Referenzimplementierung
Ab Python 2.4-alpha wurde der Code in die Standardbibliothek aufgenommen. Die neueste Version ist verfügbar unter:
http://svn.python.org/view/python/trunk/Lib/decimal.py
Die Testfälle befinden sich hier:
http://svn.python.org/view/python/trunk/Lib/test/test_decimal.py
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0327.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT