545 Stimmen

Elegante Wege zur Unterstützung von Äquivalenz ("Gleichheit") in Python-Klassen

Beim Schreiben von benutzerdefinierten Klassen ist es oft wichtig, die Äquivalenz mit Hilfe der == y != Betreiber. In Python wird dies durch die Implementierung der __eq__ y __ne__ bzw. spezielle Methoden. Der einfachste Weg, den ich gefunden habe, ist die folgende Methode:

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

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

Kennen Sie eine elegantere Möglichkeit, dies zu tun? Kennen Sie besondere Nachteile bei der Verwendung der oben genannten Methode zum Vergleich __dict__ s?

Hinweis : Eine kleine Klarstellung: Wenn __eq__ y __ne__ undefiniert sind, werden Sie dieses Verhalten vorfinden:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

Das heißt, a == b wertet aus zu False weil es wirklich läuft a is b einen Identitätstest (d. h. "Ist a das gleiche Objekt wie b ?").

Wenn __eq__ y __ne__ definiert sind, werden Sie dieses Verhalten finden (das wir anstreben):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True

469voto

Tal Weiss Punkte 8618

Betrachten Sie dieses einfache Problem:

class Number:

    def __init__(self, number):
        self.number = number

n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

Python verwendet also standardmäßig die Objektbezeichner 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

Unter Python 2 immer daran denken, die Option __ne__ auch die Funktion, da die Dokumentation Staaten:

Es gibt keine impliziten Beziehungen zwischen den Vergleichsoperatoren. Die Wahrheit von x==y impliziert nicht, dass x!=y falsch ist. Wenn also Definition von __eq__() sollte man auch definieren __ne__() so dass die Operatoren sich wie erwartet verhalten werden.

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)

n1 == n2 # True
n1 != n2 # False

Unter Python 3 ist dies nicht mehr notwendig, da die Dokumentation Staaten:

Standardmäßig, __ne__() Delegierte zu __eq__() und kehrt das Ergebnis um es sei denn, es ist NotImplemented . Es gibt keine anderen impliziten Beziehungen zwischen den Vergleichsoperatoren, zum Beispiel die Wahrheit von (x<y or x==y) impliziert nicht x<=y .

Aber das löst 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

Anmerkung: Python 2 hat zwei Arten von Klassen:

  • klassischer Stil (ou altmodisch ) Klassen, die no erben von object und die deklariert sind als class A: , class A(): o class A(B): donde B ist eine Klasse im klassischen Stil;

  • neuartiges Klassen, die erben von object und die deklariert sind als class A(object) o class A(B): donde B ist eine Klasse des neuen Stils. Python 3 hat nur New-Style-Klassen, die deklariert werden als class A: , class A(object): o class A(B): .

Bei Klassen im klassischen Stil ruft eine Vergleichsoperation immer die Methode des ersten Operanden auf, während sie bei Klassen im neuen Stil immer die Methode des Operanden der Unterklasse aufruft, unabhängig von der Reihenfolge der Operanden .

Wenn also hier Number ist eine Klasse im klassischen Stil:

  • n1 == n3 llama a n1.__eq__ ;
  • n3 == n1 llama a n3.__eq__ ;
  • n1 != n3 llama a n1.__ne__ ;
  • n3 != n1 llama a n3.__ne__ .

Und wenn Number ist eine Klasse des neuen Stils:

  • beide n1 == n3 y n3 == n1 aufrufen n3.__eq__ ;
  • beide n1 != n3 y n3 != n1 aufrufen n3.__ne__ .

Um das Problem der Nicht-Kommutativität der == y != Operatoren für Python-2-Klassen im klassischen Stil, die __eq__ y __ne__ Methoden sollten die NotImplemented Wert, wenn ein Operandentyp nicht unterstützt wird. Die Website Dokumentation definiert die 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. (Die Interpreter versucht dann die reflektierte Operation oder eine andere Fallback, abhängig vom Operator.) Sein Wahrheitswert ist true.

In diesem Fall delegiert der Betreiber die Vergleichsoperation an den reflektierte Methode der andere Operand. Die Website Dokumentation definiert reflektierte Methoden als:

Es gibt keine Versionen dieser Methoden mit vertauschten Argumenten (für die Verwendung von wenn das linke Argument die Operation nicht unterstützt, aber das rechte Argument die Operation unterstützt); stattdessen, __lt__() y __gt__() sind die gegenseitigen Reflexion, __le__() y __ge__() sind das Spiegelbild des jeweils anderen und __eq__() y __ne__() sind ihr eigenes Spiegelbild.

Das Ergebnis sieht wie folgt 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

Rücksendung der NotImplemented Wert anstelle von False ist auch für neuartige Klassen das Richtige, wenn Kommutativität der == y != Operatoren ist erwünscht, wenn die Operanden von unterschiedlichen Typen sind (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 standardmäßig gibt Python den Hash des Bezeichners des Objekts zurück. Lassen Sie uns versuchen, dies 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 wie folgt aus (ich habe am Ende einige Behauptungen 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

232voto

Algorias Punkte 2947

Bei der Vererbung muss man vorsichtig sein:

>>> class Foo:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

Prüfen Sie die Typen genauer, etwa so:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

Abgesehen davon wird Ihr Ansatz gut funktionieren, dafür sind spezielle Methoden ja da.

165voto

cdleary Punkte 66512

So wie Sie es beschreiben, habe ich es immer gemacht. Da es völlig generisch ist, können Sie immer brechen, dass Funktionalität in eine Mixin-Klasse und erben es in Klassen, wo Sie diese Funktionalität wollen.

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

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

class Foo(CommonEqualityMixin):

    def __init__(self, item):
        self.item = item

18voto

John Mee Punkte 46854

Das ist keine direkte Antwort, aber sie schien relevant genug, um angehängt zu werden, da sie gelegentlich etwas langatmige Worte erspart. Direkt aus den Docs geschnitten...


functools.total_ordering(cls)

Bei einer Klasse, die eine oder mehrere reichhaltige Methoden zur Vergleichsbestellung definiert, liefert dieser Klassendekorator den Rest. Dies vereinfacht den Aufwand für die Angabe aller möglichen umfangreichen Vergleichsoperationen:

Die Klasse muss eines der folgenden Elemente definieren __lt__() , __le__() , __gt__() , oder __ge__() . Darüber hinaus sollte die Klasse eine __eq__() Methode.

Neu in Version 2.7

@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()))

9voto

Vasil Punkte 34398

Sie müssen nicht beides außer Kraft setzen __eq__ y __ne__ können Sie nur außer Kraft setzen __cmp__ aber dies hat Auswirkungen auf das Ergebnis von ==, !==, < , > usw.

is Tests für die Objektidentität. Dies bedeutet eine is b wird sein True für den Fall, dass a und b beide den Verweis auf dasselbe Objekt enthalten. In Python halten Sie immer einen Verweis auf ein Objekt in einer Variablen, nicht das eigentliche Objekt, so dass im Wesentlichen für a ist b wahr zu sein, die Objekte in ihnen sollte in der gleichen Speicherstelle befinden. Wie und vor allem warum würden Sie gehen über dieses Verhalten überschreiben?

Edit: Das wusste ich nicht __cmp__ wurde aus Python 3 entfernt, also vermeiden Sie es.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X