Betrachten Sie dieses einfache Problem:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Daher verwendet Python standardmäßig die Objektkennungen für Vergleichsoperationen:
id(n1) # 140400634555856
id(n2) # 140400634555920
Das Überschreiben der __eq__
Funktion scheint das Problem zu lösen:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return False
n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3
Denken Sie in Python 2 immer daran, die __ne__
Funktion ebenfalls zu überschreiben , wie in der Dokumentation angegeben :
Es gibt keine impliziten Beziehungen zwischen den Vergleichsoperatoren. Die Wahrheit von x==y
bedeutet nicht, dass dies x!=y
falsch ist. Dementsprechend sollte beim Definieren __eq__()
auch definiert werden __ne__()
, dass sich die Operatoren wie erwartet verhalten.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
In Python 3 ist dies nicht mehr erforderlich, wie in der Dokumentation angegeben :
Standardmäßig __ne__()
delegiert __eq__()
und invertiert das Ergebnis, sofern dies nicht der Fall ist NotImplemented
. Es gibt keine anderen impliziten Beziehungen zwischen den Vergleichsoperatoren, zum Beispiel (x<y or x==y)
impliziert die Wahrheit von nicht x<=y
.
Das löst aber nicht alle unsere Probleme. Fügen wir eine Unterklasse hinzu:
class SubNumber(Number):
pass
n3 = SubNumber(1)
n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False
Hinweis: Python 2 hat zwei Arten von Klassen:
Klassen im klassischen Stil (oder im alten Stil ), die nicht erbenobject
und als deklariertclass A:
sindclass A():
oderclass A(B):
woB
sich eine Klasse im klassischen Stil befindet;
Klassen neuen Stils , die voneiner Klasse neuen Stilserbenobject
und alsclass A(object)
oderclass A(B):
wodeklariert sindB
. Python 3 enthält nur Klassen neuen Stils, die alsoderdeklariertclass A:
sind.class A(object):
class A(B):
Bei Klassen im klassischen Stil ruft eine Vergleichsoperation immer die Methode des ersten Operanden auf, während bei Klassen im neuen Stil unabhängig von der Reihenfolge der Operanden immer die Methode des Operanden der Unterklasse aufgerufen wird .
Also hier, wenn Number
es sich um eine Klasse im klassischen Stil handelt:
n1 == n3
Anrufe n1.__eq__
;
n3 == n1
Anrufe n3.__eq__
;
n1 != n3
Anrufe n1.__ne__
;
n3 != n1
Anrufe n3.__ne__
.
Und wenn Number
es sich um eine neue Klasse handelt:
- beide
n1 == n3
und n3 == n1
anrufen n3.__eq__
;
- beide
n1 != n3
und n3 != n1
anrufen n3.__ne__
.
Um das Nichtkommutativitätsproblem der Operatoren ==
und !=
für Python 2-Klassen im klassischen Stil zu beheben , sollten die Methoden __eq__
und __ne__
den NotImplemented
Wert zurückgeben, wenn ein Operandentyp nicht unterstützt wird. Die Dokumentation definiert den NotImplemented
Wert als:
Numerische Methoden und umfangreiche Vergleichsmethoden können diesen Wert zurückgeben, wenn sie die Operation für die angegebenen Operanden nicht implementieren. (Der Interpreter versucht dann die reflektierte Operation oder einen anderen Fallback, abhängig vom Operator.) Sein Wahrheitswert ist wahr.
In diesem Fall delegiert der Operator die Vergleichsoperation an die reflektierte Methode des anderen Operanden. In der Dokumentation werden reflektierte Methoden wie folgt definiert:
Es gibt keine Versionen dieser Methoden mit ausgetauschten Argumenten (die verwendet werden sollen, wenn das linke Argument die Operation nicht unterstützt, das rechte Argument jedoch). vielmehr __lt__()
und __gt__()
sind die Reflexion des anderen __le__()
und __ge__()
sind die Reflexion des anderen und
__eq__()
und __ne__()
sind ihre eigene Reflexion.
Das Ergebnis sieht folgendermaßen aus:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is NotImplemented:
return NotImplemented
return not x
Die Rückgabe des NotImplemented
Werts anstelle von False
ist auch für Klassen neuen Stils das Richtige, wenn die Kommutativität der Operatoren ==
und !=
gewünscht wird, wenn die Operanden nicht verwandte Typen haben (keine Vererbung).
Sind wir schon da? Nicht ganz. Wie viele eindeutige Nummern haben wir?
len(set([n1, n2, n3])) # 3 -- oops
Sets verwenden die Hashes von Objekten, und Python gibt standardmäßig den Hash des Bezeichners des Objekts zurück. Versuchen wir es zu überschreiben:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
Das Endergebnis sieht folgendermaßen aus (ich habe am Ende einige Aussagen zur Validierung hinzugefügt):
class Number:
def __init__(self, number):
self.number = number
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is not NotImplemented:
return not x
return NotImplemented
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
class SubNumber(Number):
pass
n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)
assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1
assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1
assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1
assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
is
Operator hat, um die Objektidentität vom Wertevergleich zu unterscheiden.