Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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

  1. Abwärtskompatibilität, sowohl auf Python-Ebene (Klassen, die __cmp__ verwenden, müssen nicht geändert werden) als auch auf C-Ebene (Erweiterungen, die tp_comparea definieren, müssen nicht geändert werden, Code, der PyObject_Compare() verwendet, muss auch dann funktionieren, wenn die verglichenen Objekte das neue Rich Comparison-Schema verwenden).
  2. 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.
  3. 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 <=?
  4. Ä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?
  5. 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?
  6. Was ist mit min() und max(), den 'in' und 'not in' Operatoren, list.sort(), Dictionary-Schlüsselvergleich und anderen Verwendungen von Vergleichen durch eingebaute Operationen?

Vorgeschlagene Lösungen

  1. Volle Abwärtskompatibilität kann wie folgt erreicht werden. Wenn ein Objekt tp_compare() definiert, aber nicht tp_richcompare(), und ein Rich Comparison angefordert wird, wird das Ergebnis von tp_compare() auf offensichtliche Weise verwendet. Z.B. wenn „<“ angefordert wird, wird eine Ausnahme ausgelöst, wenn tp_compare() eine Ausnahme auslöst, das Ergebnis ist 1, wenn tp_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.)

  2. 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.
  3. 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).
  4. 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.)
  5. 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.
  6. Die Operationen min() und list.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 auf Py_NotImplemented zurü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. in list.sort() und natürlich für die Vergleichsoperatoren in ceval.c), wird der Code so geändert, dass stattdessen PyObject_RichCompare() oder PyObject_RichCompareBool() aufgerufen wird; wenn der C-Code das Ergebnis des Vergleichs kennen muss, wird PyObject_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 und NotImplemented zurückgeben.

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

  1. Ä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 durch

    typedef 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.)

  2. 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.

  3. Hinzufügung eines optionalen Arguments zur eingebauten cmp() Funktion

    Die 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, kann cmp() 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