Python 3.1: __cmp__
Für den Vergleich von eigenen Objekten genügte es mit Python2 die Methode __cmp__(self, other)
zu implementieren, die einen Wert kleiner, gleich oder größer Null zurück gibt, je nach dem wie der Vergleich ausfällt.
Das erinnert sehr an die guten alten C-Zeiten.
Aus verschiedenen Gründen wurde das mit Python 3 geändert, u.a. möchte z.B. NumPy beim Vergleich von Vektoren kein einfaches bool
zurück geben, sondern einen Vektor komponentenweise vergleichen und selber einen Vektor mit dem Ergebnis pro Komponente zurückzugeben.
Das war mit der alten Syntax von __cmp__()
nicht machbar.
Schon zu Python 2-Zeiten konnte man alternativ die Rich Comparison-Operatoren __lt__|__le__|__eq__|__ne__|__ge__|__gt__(self, other)
für den Vergleich < | <= | = | != | >= | >
implementieren.
Das Implementieren der 6 statt einer Methode ist leider deutlich aufwendiger.
Hilfreich dabei ist der Dekorator für Klassen @functools.total_ordering
, womit es reicht, nur eine Methode zu implementieren, was aber zu Performance-Problemen führen kann.
Empfehlungen
__cmp__(self, other)
schon jetzt durch die anderen Methoden ersetzen.
Ansonsten läuft man Gefahr, dass der Vergleich sich unterschiedlich verhält, je nach dem ob man den Code unter Python 2 oder Python 3 ausführt.
Einen Unit-Test dafür schreiben, ob der Vergleich auch wirklich funktioniert.
Beispiel
#!/usr/bin/python3
class Person(object):
def __init__(self, lastname, firstname):
self.lastname = lastname
self.firstname = firstname
def __lt__(self, other):
"""
>>> Person("Hahn", "Philipp") < Person('Schwardt', 'Sönke')
True
"""
return (self.lastname, self.firstname) < (other.lastname, other.firstname) if isinstance(other, Person) else NotImplemented
def __le__(self, other):
return (self.lastname, self.firstname) <= (other.lastname, other.firstname) if isinstance(other, Person) else NotImplemented
def __eq__(self, other):
return isinstance(other, Person) and (self.lastname, self.firstname) == (other.lastname, other.firstname)
def __ne__(self, other):
return not isinstance(other, Person) or (self.lastname, self.firstname) != (other.lastname, other.firstname)
def __ge__(self, other):
return (self.lastname, self.firstname) >= (other.lastname, other.firstname) if isinstance(other, Person) else NotImplemented
def __gt__(self, other):
return (self.lastname, self.firstname) > (other.lastname, other.firstname) if isinstance(other, Person) else NotImplemented
if __name__ == "__main__":
import doctest
doctest.testmod()
Und mann sollte tunlichst aufpassen, dass man den Vergleich richtig implementiert:
# Falsch:
def __lt__(self, other):
"""
>>> Person("Hahn", "Philipp") < Person("Requate", "Arvid")
True
"""
return self.lastname < other.lastname or self.firstname < other.firstname
# Richtig(er):
def __lt__(self, other):
return self.lastname < other.lastname or (self.lastname == other.lastname and self.firstname < other.firstname)
# Richtig (als Alternative zu oben):
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
if self.lastname < other.lastname:
return True
if self.lastname > other.lastname:
return False
assert self.lastname == other.lastname
if self.firstname < other.firstname:
return True
assert self.firstname >= other.firstname
return False
Achtung
In diesem Zusammenhang noch ein wichtiger Unterschied:
Es gibt die Klasse NotImplementedError
und eine Konstante NotImplemented
.
Erstere (Klasse) dient dazu, damit bei abstrakten Klassen zu signalisieren, dass eine Methode (noch) nicht implementiert ist:
class AbstractBase(object):
def method(self):
raise NotImplementedError
Die zweite (Konstante) dient dazu bei Vergleich zu signalisieren, dass dieser Vergleich nicht implementiert ist:
class GeometricObject(object):
def __eq__(self, other):
if not isinstance(other, GeometricObject):
return NotImplemented
return True
class Circle(GeometricObject):
def __eq__(self, other):
return False
Der Aufruf GeometricObject() == Circle()
führt zu dem Aufruf GeometricObject().__eq__(Circle())
, was zunächst NotImplemented
liefert.
Das führt dann dazu, dass Python automatisch den Vergleich umdreht und statt dessen Circle.__eq__(GeometricObject)
eine 2. Chance gibt, den Vergleich in umgedrehter Weise nochmals durchzuführen.
Das Ergebnis ist deswegen False
.
Inzwischen erkennt Python auch eine Rekursion in diesen Aufrufen, aber man sollte trotzdem ein Auge darauf haben.
Merke:
raise NotImplemented
ist immer falschreturn NotImplementedError
ist auch falsch