__lt__ statt __cmp__

100

Python 2.x bietet zwei Möglichkeiten, um Vergleichsoperatoren __cmp__oder die "Rich-Vergleichsoperatoren" wie z __lt__. Die reichhaltigen Vergleichsüberladungen sollen bevorzugt werden, aber warum ist das so?

Umfangreiche Vergleichsoperatoren sind einfacher zu implementieren, Sie müssen jedoch mehrere davon mit nahezu identischer Logik implementieren. Wenn Sie jedoch die integrierte Reihenfolge cmpund die Tupelreihenfolge verwenden können , __cmp__wird dies recht einfach und erfüllt alle Vergleiche:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

Diese Einfachheit scheint meine Bedürfnisse viel besser zu erfüllen, als alle 6 (!) Der reichhaltigen Vergleiche zu überladen. (Sie können es jedoch auf "nur" 4 reduzieren, wenn Sie sich auf das "getauschte Argument" / reflektiertes Verhalten verlassen, aber dies führt meiner bescheidenen Meinung nach zu einer Nettoerhöhung der Komplikationen.)

Gibt es unvorhergesehene Fallstricke, auf die ich aufmerksam gemacht werden muss, wenn ich nur überlastete __cmp__?

Ich verstehe das <, <=, ==, etc. können Betreiber für andere Zwecke überlastet werden und kann zurückkehren , jedes Objekt die sie mögen. Ich frage nicht nach den Vorzügen dieses Ansatzes, sondern nur nach Unterschieden, wenn diese Operatoren für Vergleiche in demselben Sinne verwendet werden, wie sie für Zahlen bedeuten.

Update: Wie Christopher betonte , cmpverschwindet es in 3.x. Gibt es Alternativen, die die Implementierung von Vergleichen so einfach wie oben machen __cmp__?

Gemeinschaft
quelle
5
Siehe meine Antwort zu Ihrer letzten Frage, aber tatsächlich gibt es ein Design, das die Dinge für viele Klassen, einschließlich Ihrer, noch einfacher macht (im Moment benötigen Sie ein Mixin, eine Metaklasse oder einen Klassendekorateur, um es anzuwenden): Wenn eine spezielle Schlüsselmethode vorhanden ist, Es muss ein Tupel von Werten zurückgeben, und alle Komparatoren UND Hashs werden in Bezug auf dieses Tupel definiert. Guido gefiel meine Idee, als ich sie ihm erklärte, aber dann beschäftigte ich mich mit anderen Dingen und kam nie dazu, einen PEP zu schreiben ... vielleicht für 3.2 ;-). In der Zwischenzeit benutze ich weiterhin mein Mixin dafür! -)
Alex Martelli

Antworten:

90

Ja, es ist einfach, alles zu implementieren, z. B. __lt__mit einer Mixin-Klasse (oder einer Metaklasse oder einem Klassendekorateur, wenn Ihr Geschmack so ist).

Beispielsweise:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

Jetzt kann Ihre Klasse nur definieren __lt__und das Erben von ComparableMixin multiplizieren (nach allen anderen Basen, die sie benötigt, falls vorhanden). Ein Klassendekorateur wäre ziemlich ähnlich und würde nur ähnliche Funktionen als Attribute der neuen Klasse einfügen, die er dekoriert (das Ergebnis könnte zur Laufzeit mikroskopisch schneller sein, bei gleich geringen Speicherkosten).

Wenn Ihre Klasse eine besonders schnelle Möglichkeit zur Implementierung hat (z. B.) __eq__und __ne__diese direkt definieren sollte, damit die Versionen des Mixins nicht verwendet werden (z. B. für dict), kann dies __ne__möglicherweise zur Erleichterung definiert werden das als:

def __ne__(self, other):
  return not self == other

aber im obigen Code wollte ich die erfreuliche Symmetrie <beibehalten, nur zu verwenden ;-). Wie, warum __cmp__, gehen musste , da wir taten haben __lt__und Freunde, halten , warum eine andere, unterschiedliche Art und Weise genau die gleiche Sache um zu tun? Es ist einfach so viel Eigengewicht in jeder Python-Laufzeit (Classic, Jython, IronPython, PyPy, ...). Der Code, der definitiv keine Fehler aufweist, ist der Code, der nicht vorhanden ist - woher Pythons Prinzip, dass es idealerweise einen offensichtlichen Weg geben sollte, eine Aufgabe auszuführen (C hat das gleiche Prinzip im Abschnitt "Spirit of C" von der ISO-Standard übrigens).

Dies bedeutet nicht , dass wir unseren Weg gehen zu verbieten , Dinge (zB in der Nähe von -Äquivalenz zwischen Mixins und Klasse Dekorateure für einige Anwendungen), aber es definitiv tut bedeuten , dass wir nicht wie tragen Code in den Compiler und / oder redundant vorhandene Laufzeiten, nur um mehrere äquivalente Ansätze zu unterstützen, um genau dieselbe Aufgabe auszuführen.

Weitere Bearbeitung: Es gibt tatsächlich eine noch bessere Möglichkeit, Vergleiche UND Hashing für viele Klassen bereitzustellen, einschließlich der in der Frage - eine __key__Methode, wie ich in meinem Kommentar zur Frage erwähnt habe. Da ich nie dazu gekommen bin, das PEP dafür zu schreiben, müssen Sie es derzeit mit einem Mixin (& c) implementieren, wenn Sie es mögen:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

Es ist ein sehr häufiger Fall, dass Vergleiche einer Instanz mit anderen Instanzen darauf hinauslaufen, ein Tupel für jede mit einigen Feldern zu vergleichen - und dann sollte Hashing auf genau derselben Basis implementiert werden. Die __key__speziellen Methodenadressen, die direkt benötigt werden.

Alex Martelli
quelle
Entschuldigung für die Verzögerung @R. Pate, ich entschied, dass ich, da ich sowieso bearbeiten musste, die gründlichste Antwort geben sollte, die ich konnte, anstatt mich zu beeilen (und ich habe sie gerade noch einmal bearbeitet, um meine alte Schlüsselidee vorzuschlagen, die ich nie zu PEPping gebracht habe, und wie um es mit einem Mixin umzusetzen).
Alex Martelli
Ich mag diese Schlüsselidee wirklich , werde sie nutzen und sehen, wie sie sich anfühlt. (Obwohl cmp_key oder _cmp_key anstelle eines reservierten Namens genannt.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinWenn ich dies in Python 3 versuche. Den vollständigen Code finden Sie unter gist.github.com/2696496
Adam Parkin
2
In Python 2.7 + / 3.2 + können Sie verwenden, functools.total_orderinganstatt Ihre eigenen zu erstellen ComparableMixim. Wie in jmagnussons Antwort vorgeschlagen
Tag
4
Die Verwendung <zur Implementierung __eq__in Python 3 ist aus diesem Grund eine ziemlich schlechte Idee TypeError: unorderable types.
Antti Haapala
49

Um diesen Fall zu vereinfachen, gibt es in Python 2.7 + / 3.2 + einen Klassendekorator , functools.total_ordering , mit dem implementiert werden kann, was Alex vorschlägt. Beispiel aus den Dokumenten:

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))
jmagnusson
quelle
9
total_orderingimplementiert aber nicht __ne__, also aufgepasst!
Flimm
3
@Flimm tut es nicht, aber __ne__. Dies liegt jedoch daran, dass die __ne__Standardimplementierung delegiert wird __eq__. Hier gibt es also nichts zu beachten.
Jan Hudec
muss mindestens eine Bestelloperation definieren: <> <=> = .... eq wird nicht als Gesamtbestellung benötigt, wenn! a <b und b <a dann a = b
Xanlantos
9

Dies wird durch PEP 207 - Rich Comparisons abgedeckt

Auch __cmp__geht in Python 3.0 weg. (Beachten Sie, dass es nicht auf http://docs.python.org/3.0/reference/datamodel.html vorhanden ist, sondern auf http://docs.python.org/2.7/reference/datamodel.html )

Christopher
quelle
Das PEP befasst sich nur damit, warum umfangreiche Vergleiche erforderlich sind, so wie NumPy-Benutzer möchten, dass A <B eine Sequenz zurückgibt.
Ich hatte nicht bemerkt, dass es definitiv weggeht, das macht mich traurig. (Aber danke, dass Sie darauf hingewiesen haben.)
Das PEP diskutiert auch, warum sie bevorzugt werden. Im Wesentlichen läuft es auf Effizienz hinaus: 1. Es müssen keine Operationen implementiert werden, die für Ihr Objekt keinen Sinn ergeben (z. B. ungeordnete Sammlungen). 2. Einige Sammlungen verfügen über sehr effiziente Operationen für bestimmte Arten von Vergleichen. Durch umfangreiche Vergleiche kann der Interpreter dies nutzen, wenn Sie sie definieren.
Christopher
1
Zu 1: Wenn sie keinen Sinn ergeben, implementieren Sie cmp nicht . Zu 2 können Sie mit beiden Optionen nach Bedarf optimieren und gleichzeitig schnell Prototypen erstellen und testen. Keiner sagt mir, warum es entfernt wurde. (Im Wesentlichen läuft es für mich auf die Entwicklereffizienz hinaus.) Ist es möglich, dass die umfangreichen Vergleiche mit dem vorhandenen cmp- Fallback weniger effizient sind ? Das würde für mich keinen Sinn ergeben.
1
@R. Pate, wie ich in meiner Antwort zu erklären versuche, gibt es keinen wirklichen Verlust an Allgemeinheit (da Sie mit einem Mixin, Dekorator oder einer Metaklasse einfach alles in Form von <definieren können, wenn Sie dies wünschen) und damit alle Python-Implementierungen mit sich herumtragen können Redundanter Code, der für immer auf cmp zurückgreift - nur damit Python-Benutzer Dinge auf zwei gleichwertige Arten ausdrücken können - würde zu 100% gegen Python laufen.
Alex Martelli
2

(Bearbeitet am 17.06.17, um Kommentare zu berücksichtigen.)

Ich habe die vergleichbare Mixin-Antwort oben ausprobiert. Ich hatte Probleme mit "None". Hier ist eine modifizierte Version, die Gleichheitsvergleiche mit "Keine" behandelt. (Ich sah keinen Grund, mich mit Ungleichheitsvergleichen mit None als nicht semantisch zu beschäftigen):


class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other and not other<self

    def __ne__(self, other):
        return not __eq__(self, other)

    def __gt__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not other<self    
Gabriel Ferrer
quelle
Wie denkst du, selfkönnte das der Singleton Nonevon dir sein NoneTypeund gleichzeitig dein implementieren ComparableMixin? Und in der Tat ist dieses Rezept schlecht für Python 3.
Antti Haapala
3
selfwird nie sein None, so dass Zweig ganz gehen kann. Nicht benutzen type(other) == type(None); einfach benutzen other is None. NoneTesten Sie anstelle eines speziellen Gehäuses , ob der andere Typ eine Instanz des Typs von ist self, und geben Sie den NotImplementedSingleton zurück, wenn nicht : if not isinstance(other, type(self)): return NotImplemented. Tun Sie dies für alle Methoden. Python kann dann dem anderen Operanden die Möglichkeit geben, stattdessen eine Antwort zu geben.
Martijn Pieters
1

Inspiriert von Alex Martellis ComparableMixin& KeyedMixinAntworten, habe ich mir das folgende Mixin ausgedacht. Sie können eine einzelne _compare_to()Methode implementieren , die schlüsselbasierte Vergleiche ähnlich wie verwendet KeyedMixin, aber Ihrer Klasse ermöglicht, den effizientesten Vergleichsschlüssel basierend auf dem Typ von auszuwählen other. (Beachten Sie, dass dieses Mixin für Objekte, die auf Gleichheit, aber nicht auf Reihenfolge getestet werden können, nicht viel hilft.)

class ComparableMixin(object):
    """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
        """return keys to compare self to other.

        if self and other are comparable, this function 
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
        """
        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

    def __ne__(self, other):
        return not self == other

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented
Eli Collins
quelle