PEP 207 – Rich Comparisons
- Autor:
- Guido van Rossum <guido at python.org>, David Ascher <DavidA at ActiveState.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 25-Jul-2000
- Python-Version:
- 2.1
- Post-History:
Inhaltsverzeichnis
Zusammenfassung
Dieses PEP schlägt mehrere neue Funktionen für Vergleiche vor
- Erlaube separate Überladung von <, >, <=, >=, ==, !=, sowohl in Klassen als auch in C-Erweiterungen.
- Erlaube es jedem dieser überladenen Operatoren, etwas anderes als ein boolesches Ergebnis zurückzugeben.
Motivation
Die Hauptmotivation kommt von NumPy, deren Benutzer zustimmen, dass A<B ein Array von elementweisen Vergleichsergebnissen zurückgeben sollte; sie müssen dies derzeit als less(A,B) schreiben, da A<B nur ein boolesches Ergebnis zurückgeben oder eine Ausnahme auslösen kann.
Eine zusätzliche Motivation ist, dass Typen oft keine natürliche Ordnung haben, aber trotzdem auf Gleichheit verglichen werden müssen. Derzeit muss ein solcher Typ muss Vergleiche implementieren und damit eine willkürliche Ordnung definieren, nur damit die Gleichheit getestet werden kann.
Außerdem kann für einige Objekttypen ein Gleichheitstest viel effizienter implementiert werden als ein Ordnungstest; zum Beispiel sind Listen und Dictionaries, die sich in der Länge unterscheiden, ungleich, aber die Ordnung erfordert die Inspektion einiger (potenziell aller) Elemente.
Vorangegangene Arbeiten
Rich Comparisons wurden bereits vorgeschlagen; insbesondere von David Ascher, nach Erfahrungen mit Numerical Python
Er ist auch unten als Anhang enthalten. Die meisten Materialien in diesem PEP stammen aus Davids Vorschlag.
Bedenken
- Abwärtskompatibilität, sowohl auf Python-Ebene (Klassen, die
__cmp__verwenden, müssen nicht geändert werden) als auch auf C-Ebene (Erweiterungen, dietp_compareadefinieren, müssen nicht geändert werden, Code, derPyObject_Compare()verwendet, muss auch dann funktionieren, wenn die verglichenen Objekte das neue Rich Comparison-Schema verwenden). - Wenn A<B eine Matrix elementweiser Vergleiche zurückgibt, ist ein einfacher Fehler, diesen Ausdruck in einem booleschen Kontext zu verwenden. Ohne spezielle Vorkehrungen wäre er immer wahr. Diese Verwendung sollte stattdessen eine Ausnahme auslösen.
- Wenn eine Klasse x==y überschreibt, aber nichts anderes, sollte x!=y als not(x==y) berechnet werden oder fehlschlagen? Was ist mit der ähnlichen Beziehung zwischen < und >=, oder zwischen > und <=?
- Ähnlich, sollten wir x<y aus y>x berechnen lassen? Und x<=y aus not(x>y)? Und x==y aus y==x, oder x!=y aus y!=x?
- Wenn Vergleichsoperatoren elementweise Vergleiche zurückgeben, was ist mit Shortcut-Operatoren wie A<B<C,
A<B and C<D,A<B or C<D? - Was ist mit
min()undmax(), den 'in' und 'not in' Operatoren,list.sort(), Dictionary-Schlüsselvergleich und anderen Verwendungen von Vergleichen durch eingebaute Operationen?
Vorgeschlagene Lösungen
- Volle Abwärtskompatibilität kann wie folgt erreicht werden. Wenn ein Objekt
tp_compare()definiert, aber nichttp_richcompare(), und ein Rich Comparison angefordert wird, wird das Ergebnis vontp_compare()auf offensichtliche Weise verwendet. Z.B. wenn „<“ angefordert wird, wird eine Ausnahme ausgelöst, wenntp_compare()eine Ausnahme auslöst, das Ergebnis ist 1, wenntp_compare()negativ ist, und 0, wenn es null oder positiv ist. Usw.Volle Vorwärtskompatibilität kann wie folgt erreicht werden. Wenn ein klassischer Vergleich für ein Objekt angefordert wird, das
tp_richcompare()implementiert, werden bis zu drei Vergleiche verwendet: zuerst wird == versucht, und wenn es wahr zurückgibt, wird 0 zurückgegeben; als nächstes wird < versucht und wenn es wahr zurückgibt, wird -1 zurückgegeben; als nächstes wird > versucht und wenn es wahr zurückgibt, wird +1 zurückgegeben. Wenn ein versuchter Operator einen nicht-booleschen Wert zurückgibt (siehe unten), wird die durch Konvertierung in Boolean ausgelöste Ausnahme weitergegeben. Wenn keiner der versuchten Operatoren wahr zurückgibt, werden als nächstes die klassischen Vergleichs-Fallbacks versucht.(Ich habe lange und intensiv über die Reihenfolge nachgedacht, in der die drei Vergleiche versucht werden sollten. An einem Punkt hatte ich ein überzeugendes Argument dafür, dies in dieser Reihenfolge zu tun, basierend auf dem Verhalten von Vergleichen für zyklische Datenstrukturen. Aber seit dieser Code wieder geändert wurde, bin ich mir nicht mehr so sicher, ob es einen Unterschied macht.)
- Jeder Typ, der eine Sammlung von Booleans anstelle eines einzelnen Booleans zurückgibt, sollte
nb_nonzero()definieren, um eine Ausnahme auszulösen. Ein solcher Typ wird als nicht-boolesch betrachtet. - Die Operatoren == und != werden nicht als gegenseitig komplementär angenommen (z. B. erfüllen IEEE 754 Gleitkommazahlen dies nicht). Es liegt am Typ, dies bei Bedarf zu implementieren. Ähnlich für < und >=, oder > und <=; es gibt viele Beispiele, bei denen diese Annahmen nicht zutreffen (z. B. tabnanny).
- Die Reflexivitätsregeln werden von Python angenommen. Daher kann der Interpreter y>x mit x<y vertauschen, y>=x mit x<=y, und die Argumente von x==y und x!=y vertauschen. (Hinweis: Python nimmt derzeit an, dass x==x immer wahr und x!=x nie wahr ist; dies sollte nicht angenommen werden.)
- Im aktuellen Vorschlag wird, wenn A<B ein Array von elementweisen Vergleichen zurückgibt, dieses Ergebnis als nicht-boolesch betrachtet, und seine Interpretation als boolesch durch die Shortcut-Operatoren löst eine Ausnahme aus. David Aschers Vorschlag versucht, dies zu behandeln; ich denke nicht, dass dies die zusätzliche Komplexität im Code-Generator wert ist. Anstelle von A<B<C kann man (A<B)&(B<C) schreiben.
- Die Operationen
min()undlist.sort()werden nur den Operator < verwenden; max() wird nur den Operator > verwenden. Die Operatoren 'in' und 'not in' und die Dictionary-Suche werden nur den Operator == verwenden.
Implementierungsvorschlag
Dies folgt eng David Aschers Vorschlag.
C API
- Neue Funktionen
PyObject *PyObject_RichCompare(PyObject *, PyObject *, int)
Dies führt den angeforderten Rich Comparison durch und gibt ein Python-Objekt zurück oder löst eine Ausnahme aus. Das 3. Argument muss eines von Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT oder Py_GE sein.
int PyObject_RichCompareBool(PyObject *, PyObject *, int)
Dies führt den angeforderten Rich Comparison durch und gibt einen Boolean zurück: -1 für Ausnahme, 0 für falsch, 1 für wahr. Das 3. Argument muss eines von Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT oder Py_GE sein. Beachten Sie, dass, wenn
PyObject_RichCompare()ein nicht-boolesches Objekt zurückgibt,PyObject_RichCompareBool()eine Ausnahme auslöst. - Neuer typedef
typedef PyObject *(*richcmpfunc) (PyObject *, PyObject *, int);
- Neuer Slot im Typobjekt, ersetzt den freien tp_xxx7
richcmpfunc tp_richcompare;
Dies sollte eine Funktion mit der gleichen Signatur wie
PyObject_RichCompare()sein und den gleichen Vergleich durchführen. Mindestens eines der Argumente ist vom Typ, dessen tp_richcompare Slot verwendet wird, aber das andere kann einen anderen Typ haben. Wenn die Funktion die spezifische Kombination von Objekten nicht vergleichen kann, sollte sie eine neue Referenz aufPy_NotImplementedzurückgeben. PyObject_Compare()wird so geändert, dass Rich Comparisons versucht werden, wenn sie definiert sind (aber nur, wenn klassische Comparisons nicht definiert sind).
Änderungen am Interpreter
- Immer wenn
PyObject_Compare()mit der Absicht aufgerufen wird, das Ergebnis eines bestimmten Vergleichs zu erhalten (z.B. inlist.sort()und natürlich für die Vergleichsoperatoren in ceval.c), wird der Code so geändert, dass stattdessenPyObject_RichCompare()oderPyObject_RichCompareBool()aufgerufen wird; wenn der C-Code das Ergebnis des Vergleichs kennen muss, wirdPyObject_IsTrue()auf das Ergebnis aufgerufen (was eine Ausnahme auslösen kann). - Die meisten eingebauten Typen, die derzeit einen Vergleich definieren, werden so modifiziert, dass sie stattdessen einen Rich Comparison definieren. (Dies ist optional; ich habe bisher Listen, Tupel, komplexe Zahlen und Arrays konvertiert, und bin mir nicht sicher, ob ich andere konvertieren werde.)
Klassen
- Klassen können neue spezielle Methoden
__lt__,__le__,__eq__,__ne__,__gt__,__ge__definieren, um die entsprechenden Operatoren zu überschreiben. (D.h. <, <=, ==, !=, >, >=. Man muss die Fortran-Herkunft lieben.) Wenn eine Klasse auch__cmp__definiert, wird sie nur verwendet, wenn__lt__usw. versucht wurden undNotImplementedzurückgeben.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Anhang
Hier ist der größte Teil von David Aschers ursprünglichem Vorschlag (Version 0.2.1, datiert Mi Jul 22 16:49:28 1998; ich habe die Abschnitte Inhaltsverzeichnis, Verlauf und Patches weggelassen). Er behandelt fast alle oben genannten Bedenken.
Zusammenfassung
Ein neuer Mechanismus, der es Vergleichen von Python-Objekten ermöglicht, Werte außer -1, 0 oder 1 zurückzugeben (oder Ausnahmen auszulösen), wird vorgeschlagen. Dieser Mechanismus ist vollständig abwärtskompatibel und kann auf der Ebene des C PyObject-Typs oder der Python-Klassendefinition gesteuert werden. Es gibt drei kooperierende Teile des vorgeschlagenen Mechanismus
- die Verwendung des letzten Slots in der Typobjektstruktur zur Speicherung eines Zeigers auf eine Rich Comparison-Funktion
- die Hinzufügung von speziellen Methoden für Klassen
- die Hinzufügung eines optionalen Arguments zur eingebauten
cmp()Funktion.
Motivation
Das aktuelle Vergleichsprotokoll für Python-Objekte geht davon aus, dass zwei beliebige Python-Objekte verglichen werden können (seit Python 1.5 können Objektvergleiche Ausnahmen auslösen) und dass der Rückgabewert für jeden Vergleich -1, 0 oder 1 sein soll. -1 zeigt an, dass das erste Argument der Vergleichsfunktion kleiner ist als das rechte, +1 die kontrare positive, und 0, dass die beiden Objekte gleich sind. Während dieser Mechanismus die Festlegung einer Ordnungsbeziehung ermöglicht (z. B. für die Verwendung durch die sort() Methode von Listenobjekten), hat er sich im Kontext von Numeric Python (NumPy) als begrenzt erwiesen.
Insbesondere erlaubt NumPy die Erstellung mehrdimensionaler Arrays, die die meisten numerischen Operatoren unterstützen. Somit
x = array((1,2,3,4)) y = array((2,2,4,4))
sind zwei NumPy-Arrays. Während sie elementweise addiert werden können,
z = x + y # z == array((3,4,7,8))
können sie im aktuellen Framework nicht verglichen werden – die veröffentlichte Version von NumPy vergleicht die Zeiger (wodurch fehlerhafte Informationen entstehen), was die einzige Lösung vor der kürzlichen Hinzufügung der Fähigkeit (in 1.5) war, Ausnahmen in Vergleichsfunktionen auszulösen.
Selbst mit der Fähigkeit, Ausnahmen auszulösen, macht das aktuelle Protokoll Array-Vergleiche nutzlos. Um dieser Tatsache Rechnung zu tragen, enthält NumPy mehrere Funktionen, die die Vergleiche durchführen: less(), less_equal(), greater(), greater_equal(), equal(), not_equal(). Diese Funktionen geben Arrays mit der gleichen Form wie ihre Argumente zurück (modulo Broadcasting), gefüllt mit 0en und 1en, je nachdem, ob der Vergleich für jedes Elementpaar wahr oder falsch ist. So zum Beispiel mit den oben definierten Arrays x und y
less(x,y)
wäre ein Array, das die Zahlen (1,0,0,0) enthält.
Der aktuelle Vorschlag ist, die Python-Objektschnittstelle zu ändern, damit das NumPy-Paket x < y das gleiche zurückgibt wie less(x,y). Der genaue Rückgabewert liegt beim NumPy-Paket – was dieser Vorschlag wirklich verlangt, ist die Änderung des Python-Kerns, damit Erweiterungsobjekte die Möglichkeit haben, etwas anderes als -1, 0, 1 zurückzugeben, wenn ihre Autoren dies wünschen.
Aktueller Stand der Dinge
Das aktuelle Protokoll besteht auf C-Ebene darin, dass jeder Objekttyp einen tp_compare Slot definiert, der ein Zeiger auf eine Funktion ist, die zwei PyObject* Referenzen entgegennimmt und -1, 0 oder 1 zurückgibt. Diese Funktion wird von der PyObject_Compare() Funktion aufgerufen, die in der C API definiert ist. PyObject_Compare() wird auch von der eingebauten Funktion cmp() aufgerufen, die zwei Argumente entgegennimmt.
Vorgeschlagener Mechanismus
- Änderungen an der C-Struktur für Typobjekte
Der letzte verfügbare Slot im
PyTypeObject, der bisher für zukünftige Erweiterungen reserviert war, wird verwendet, um optional einen Zeiger auf eine neue Vergleichsfunktion vom Typ richcmpfunc zu speichern, definiert durchtypedef PyObject *(*richcmpfunc) Py_PROTO((PyObject *, PyObject *, int));
Diese Funktion nimmt drei Argumente entgegen. Die ersten beiden sind die zu vergleichenden Objekte, und das dritte ist eine Ganzzahl, die einem Opcode entspricht (einer von LT, LE, EQ, NE, GT, GE). Wenn dieser Slot NULL gelassen wird, dann wird Rich Comparison für diesen Objekttyp nicht unterstützt (außer für Klasseninstanzen, deren Klasse die unten beschriebenen speziellen Methoden bereitstellt).
Die obigen Opcodes müssen zur veröffentlichten Python/C API hinzugefügt werden (wahrscheinlich unter den Namen Py_LT, Py_LE, etc.)
- Hinzufügung von speziellen Methoden für Klassen
Klassen, die den Rich Comparison-Mechanismus unterstützen wollen, müssen eine oder mehrere der folgenden neuen speziellen Methoden hinzufügen
def __lt__(self, other): ... def __le__(self, other): ... def __gt__(self, other): ... def __ge__(self, other): ... def __eq__(self, other): ... def __ne__(self, other): ...
Jede dieser Methoden wird aufgerufen, wenn die Klasseninstanz sich auf der linken Seite der entsprechenden Operatoren (<, <=, >, >=, == und != oder <>) befindet. Das Argument other wird auf das Objekt auf der rechten Seite des Operators gesetzt. Der Rückgabewert dieser Methoden liegt beim Implementator der Klasse (das ist schließlich der Sinn des Vorschlags).
Wenn das Objekt auf der linken Seite des Operators keinen geeigneten Rich Comparison-Operator definiert (entweder auf C-Ebene oder mit einer der speziellen Methoden), dann wird der Vergleich umgekehrt und der rechte Operator mit dem entgegengesetzten Operator aufgerufen, und die beiden Objekte werden vertauscht. Dies nimmt an, dass a < b und b > a äquivalent sind, ebenso wie a <= b und b >= a, und dass == und != kommutativ sind (z. B. a == b genau dann, wenn b == a).
Zum Beispiel, wenn obj1 ein Objekt ist, das das Rich Comparison-Protokoll unterstützt, und x und y Objekte sind, die das Rich Comparison-Protokoll nicht unterstützen, dann ruft obj1 < x die
__lt__Methode von obj1 mit x als zweitem Argument auf. x < obj1 ruft die__gt__Methode von obj1 mit x als zweitem Argument auf, und x < y verwendet einfach den bestehenden (nicht-reichen) Vergleichsmechanismus.Der oben genannte Mechanismus ist so beschaffen, dass Klassen damit durchkommen, weder
__lt__und__le__noch__gt__und__ge__zu implementieren. Weitere Intelligenz hätte in den Vergleichsmechanismus eingebaut werden können, aber diese begrenzte Menge von erlaubten "Vertauschungen" wurde gewählt, weil sie keine Infrastruktur für die Verarbeitung (Negation) von Rückgabewerten erfordert. Die Wahl von sechs speziellen Methoden gegenüber einer einzigen (z. B.__richcmp__) Methode wurde getroffen, um die Dispatche auf dem Opcode auf der Ebene der C-Implementierung statt der benutzerdefinierten Methode zu ermöglichen. - Hinzufügung eines optionalen Arguments zur eingebauten
cmp()FunktionDie eingebaute
cmp()Funktion wird weiterhin für einfache Vergleiche verwendet. Für Rich Comparisons wird sie mit einem dritten Argument aufgerufen, einem von "<", "<=", ">", ">=", "==", "!=", "<>" (die letzten beiden haben die gleiche Bedeutung). Wenn mit einer dieser Zeichenketten als drittem Argument aufgerufen, kanncmp()jedes Python-Objekt zurückgeben. Andernfalls kann sie nur wie bisher -1, 0 oder 1 zurückgeben.
Verkettete Vergleiche
Problem
Es wäre schön, Objekten, für die der Vergleich etwas anderes als -1, 0 oder 1 zurückgibt, die Verwendung in verketteten Vergleichen zu ermöglichen, wie z.B.
x < y < z
Derzeit wird dies von Python interpretiert als
temp1 = x < y
if temp1:
return y < z
else:
return temp1
Beachten Sie, dass dies das Testen des Wahrheitswerts des Vergleichsergebnisses erfordert, mit potenziellen "Shortcuts" bei der Prüfung der rechten Seite. Mit anderen Worten, der Wahrheitswert des Ergebnisses des Vergleichsergebnisses bestimmt das Ergebnis einer verketteten Operation. Dies ist problematisch im Fall von Arrays, denn wenn x, y und z drei Arrays sind, dann erwartet der Benutzer
x < y < z
ein Array von 0en und 1en zu sein, wobei 1en sich an den Positionen befinden, die den Elementen von y entsprechen, die zwischen den entsprechenden Elementen in x und z liegen. Mit anderen Worten, die rechte Seite muss ausgewertet werden, unabhängig vom Ergebnis von x < y, was mit dem derzeit vom Parser verwendeten Mechanismus unvereinbar ist.
Lösung
Guido erwähnte, dass ein möglicher Ausweg darin bestehen würde, den vom Parser für verkettete Vergleiche erzeugten Code zu ändern, um Arrays intelligent verkettet vergleichen zu lassen. Was folgt, ist eine Mischung aus seiner Idee und meinen Vorschlägen. Der Code, der für x < y < z erzeugt wird, wäre äquivalent zu
temp1 = x < y
if temp1:
temp2 = y < z
return boolean_combine(temp1, temp2)
else:
return temp1
wobei boolean_combine eine neue Funktion ist, die etwas Ähnliches wie das Folgende tut
def boolean_combine(a, b):
if hasattr(a, '__boolean_and__') or \
hasattr(b, '__boolean_and__'):
try:
return a.__boolean_and__(b)
except:
return b.__boolean_and__(a)
else: # standard behavior
if a:
return b
else:
return 0
wobei die spezielle Methode __boolean_and__ für C-Level-Typen durch einen anderen Wert des dritten Arguments der richcmp-Funktion implementiert wird. Diese Methode würde einen booleschen Vergleich der Arrays durchführen (derzeit im umath-Modul als logical_and ufunc implementiert).
Somit sollten Objekte, die von Rich Comparisons zurückgegeben werden, immer als wahr getestet werden, aber eine andere spezielle Methode definieren, die boolesche Kombinationen von ihnen und ihrem Argument erstellt.
Diese Lösung hat den Vorteil, dass verkettete Vergleiche für Arrays funktionieren, aber den Nachteil, dass Vergleichsarrays immer als wahr getestet werden müssen (in einer idealen Welt würde ich sie immer eine Ausnahme bei der Wahrheitsprüfung auslösen lassen, da die Bedeutung des Tests "if a>b:" massiv mehrdeutig ist.
Die bereits vorhandene Inlining, die Ganzzahlvergleiche behandelt, würde weiterhin gelten, was für die häufigsten Fälle keine Leistungskosten verursacht.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0207.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT