Elegante Möglichkeiten zur Unterstützung der Äquivalenz („Gleichheit“) in Python-Klassen

421

Beim Schreiben von benutzerdefinierten Klassen ist es häufig wichtig, die Äquivalenz mithilfe der Operatoren ==und zuzulassen !=. In Python wird dies durch die Implementierung der jeweiligen __eq__bzw. der __ne__speziellen Methode ermöglicht. Der einfachste Weg, dies zu tun, 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 elegantere Mittel, um dies zu tun? Kennen Sie besondere Nachteile bei der Verwendung der oben genannten Methode zum Vergleichen von __dict__s?

Hinweis : Ein bisschen Klarstellung - wenn __eq__und __ne__undefiniert, finden Sie dieses Verhalten:

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

Das heißt, a == bbewertet, Falseweil es wirklich läuft a is b, einen Identitätstest (dh "Ist adas gleiche Objekt wie b?").

Wenn __eq__und __ne__definiert sind, finden Sie dieses Verhalten (das ist das, nach dem wir suchen):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
gotgenes
quelle
6
+1, da ich nicht wusste, dass das Diktat für == die Gleichheit der Mitglieder verwendet, hatte ich angenommen, dass es sie nur für die gleichen Objektdiktate gleich zählt. Ich denke, dies ist offensichtlich, da Python den isOperator hat, um die Objektidentität vom Wertevergleich zu unterscheiden.
SingleNegationElimination
5
Ich denke, die akzeptierte Antwort muss korrigiert oder der Antwort von Algorias zugewiesen werden, so dass die strenge Typprüfung implementiert wird.
Max
1
Stellen Sie
Alex Punnen

Antworten:

327

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==ybedeutet nicht, dass dies x!=yfalsch 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 erbenobjectund als deklariertclass A:sindclass A():oderclass A(B):woBsich eine Klasse im klassischen Stil befindet;

  • Klassen neuen Stils , die voneiner Klasse neuen Stilserbenobjectund 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 Numberes sich um eine Klasse im klassischen Stil handelt:

  • n1 == n3Anrufe n1.__eq__;
  • n3 == n1Anrufe n3.__eq__;
  • n1 != n3Anrufe n1.__ne__;
  • n3 != n1Anrufe n3.__ne__.

Und wenn Numberes sich um eine neue Klasse handelt:

  • beide n1 == n3und n3 == n1anrufen n3.__eq__;
  • beide n1 != n3und n3 != n1anrufen 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 NotImplementedWert zurückgeben, wenn ein Operandentyp nicht unterstützt wird. Die Dokumentation definiert den NotImplementedWert 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 NotImplementedWerts anstelle von Falseist 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
Tal Weiss
quelle
3
hash(tuple(sorted(self.__dict__.items())))funktioniert nicht, wenn sich unter den Werten von nicht hashbare Objekte befinden self.__dict__(dh wenn eines der Attribute des Objekts beispielsweise auf a gesetzt ist list).
Max
3
Stimmt, aber wenn Sie solche veränderlichen Objekte in Ihren vars () haben, sind die beiden Objekte nicht wirklich gleich ...
Tal Weiss
12
Große Zusammenfassung, aber Sie sollten implementieren __ne__verwenden ==statt__eq__ .
Florian Brucker
1
Drei Anmerkungen: 1. In Python 3 muss keine Implementierung __ne__mehr durchgeführt werden: "Standardmäßig wird das Ergebnis an das Ergebnis __ne__()delegiert __eq__()und invertiert, sofern dies nicht der Fall ist. NotImplemented" 2. Wenn man noch implementieren möchte, __ne__ist eine allgemeinere Implementierung (die von Python 3, glaube ich) : x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Die gegebenen __eq__und __ne__Implementierungen sind suboptimal: if isinstance(other, type(self)):gibt 22 __eq__und 10 __ne__Aufrufe, während if isinstance(self, type(other)):16 __eq__und 6 __ne__Aufrufe geben würden .
Maggyero
4
Er fragte nach Eleganz, wurde aber robust.
GregNash
201

Sie müssen mit der Vererbung 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

Überprüfen Sie die Typen strenger wie folgt:

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 gibt es spezielle Methoden.

Algorias
quelle
Das ist ein guter Punkt. Ich nehme an, es ist erwähnenswert, dass die in Typen eingebaute Unterklassifizierung immer noch Gleichheit in beide Richtungen zulässt. Daher kann es sogar unerwünscht sein, zu überprüfen, ob es sich um denselben Typ handelt.
Gotgenes
12
Ich würde vorschlagen, NotImplemented zurückzugeben, wenn die Typen unterschiedlich sind, und den Vergleich an die rhs zu delegieren.
Max
4
Der @ max-Vergleich wird nicht unbedingt auf der linken Seite (LHS) zur rechten Seite (RHS) und dann auf der rechten Seite (LHS) durchgeführt. Siehe stackoverflow.com/a/12984987/38140 . Die Rückkehr, NotImplementedwie Sie vorschlagen, führt jedoch immer zu superclass.__eq__(subclass)dem gewünschten Verhalten.
Gotgenes
4
Wenn Sie eine Menge Mitglieder haben und nicht viele Objektkopien herumstehen, ist es normalerweise gut, einen ersten Identitätstest hinzuzufügen if other is self. Dies vermeidet den längeren Wörterbuchvergleich und kann eine enorme Ersparnis bedeuten, wenn Objekte als Wörterbuchschlüssel verwendet werden.
Dane White
2
Und vergessen Sie nicht zu implementieren__hash__()
Dane White
161

Die Art, wie Sie beschreiben, ist die Art, wie ich es immer getan habe. Da es vollständig generisch ist, können Sie diese Funktionalität jederzeit in eine Mixin-Klasse aufteilen und in Klassen erben, in denen Sie diese Funktionalität wünschen.

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
cdleary
quelle
6
+1: Strategiemuster zum einfachen Ersetzen in Unterklassen.
S.Lott
3
isinstance saugt. Warum es überprüfen? Warum nicht einfach selbst .__ dict__ == other .__ dict__?
Nosklo
3
@nosklo: Ich verstehe nicht .. was ist, wenn zwei Objekte aus völlig unabhängigen Klassen die gleichen Attribute haben?
Max
1
Ich dachte, Nokia schlug vor, die Instanz zu überspringen. In diesem Fall wissen Sie nicht mehr, ob otheres sich um eine Unterklasse von handelt self.__class__.
Max
10
Ein weiteres Problem beim __dict__Vergleich ist, wenn Sie ein Attribut haben, das Sie bei Ihrer Definition der Gleichheit nicht berücksichtigen möchten (z. B. eine eindeutige Objekt-ID oder Metadaten wie einen Zeitstempel).
Adam Parkin
14

Keine direkte Antwort, schien aber relevant genug, um angegangen zu werden, da dies gelegentlich ein wenig langwierige Langeweile erspart. Schneiden Sie direkt aus den Dokumenten ...


functools.total_ordering (cls)

Bei einer Klasse, die eine oder mehrere umfangreiche Vergleichsordnungsmethoden definiert, liefert dieser Klassendekorateur den Rest. Dies vereinfacht den Aufwand für die Angabe aller möglichen umfangreichen Vergleichsoperationen:

Die Klasse muss eine von definieren __lt__(), __le__(), __gt__(), oder __ge__(). Zusätzlich sollte die Klasse eine __eq__()Methode bereitstellen .

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()))
John Mee
quelle
1
Total_ordering weist jedoch subtile Fallstricke auf: regebro.wordpress.com/2010/12/13/… . Sei vorsichtig !
Mr_and_Mrs_D
8

Sie müssen nicht beide überschreiben __eq__und __ne__können nur überschreiben __cmp__, dies hat jedoch Auswirkungen auf das Ergebnis von == ,! ==, <,> usw.

isTests für die Objektidentität. Dies bedeutet, dass a isb Trueder Fall ist, wenn a und b beide die Referenz auf dasselbe Objekt enthalten. In Python enthalten Sie immer einen Verweis auf ein Objekt in einer Variablen, nicht auf das eigentliche Objekt. Wenn also a b wahr ist, sollten sich die darin enthaltenen Objekte im Wesentlichen an derselben Speicherstelle befinden. Wie und vor allem warum sollten Sie dieses Verhalten außer Kraft setzen?

Bearbeiten: Ich wusste nicht, dass __cmp__aus Python 3 entfernt wurde, also vermeiden Sie es.

Vasil
quelle
Denn manchmal haben Sie eine andere Definition der Gleichheit für Ihre Objekte.
Ed S.
Der Operator is gibt Ihnen die Antwort des Interpreten auf die Objektidentität, aber Sie können Ihre
Vasil
7
In Python 3 ist "die Funktion cmp () weg und die spezielle Methode __cmp __ () wird nicht mehr unterstützt." is.gd/aeGv
gotgenes
4

Aus dieser Antwort: https://stackoverflow.com/a/30676267/541136 Ich habe gezeigt, dass es zwar richtig ist, __ne__Begriffe zu definieren __eq__- statt

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

du solltest benutzen:

def __ne__(self, other):
    return not self == other
Aaron Hall
quelle
2

Ich denke, dass die beiden Begriffe, nach denen Sie suchen, Gleichheit (==) und Identität (ist) sind. Zum Beispiel:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object
zu viel php
quelle
1
Vielleicht, außer dass man eine Klasse erstellen kann, die nur die ersten beiden Elemente in zwei Listen vergleicht, und wenn diese Elemente gleich sind, wird sie als True ausgewertet. Das ist Äquivalenz, denke ich, nicht Gleichheit. Perfekt gültig in Gl .
Gotgenes
Ich stimme jedoch zu, dass "ist" ein Identitätstest ist.
Gotgenes
1

Der 'is'-Test testet die Identität mit der eingebauten' id () '- Funktion, die im Wesentlichen die Speicheradresse des Objekts zurückgibt und daher nicht überladbar ist.

Wenn Sie jedoch die Gleichheit einer Klasse testen möchten, möchten Sie Ihre Tests wahrscheinlich etwas strenger gestalten und nur die Datenattribute in Ihrer Klasse vergleichen:

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

Dieser Code vergleicht nur Nicht-Funktionsdaten-Mitglieder Ihrer Klasse und überspringt alles Private, was im Allgemeinen gewünscht wird. Im Fall von einfachen alten Python-Objekten habe ich eine Basisklasse, die __init__, __str__, __repr__ und __eq__ implementiert, sodass meine POPO-Objekte nicht die Last all dieser zusätzlichen (und in den meisten Fällen identischen) Logik tragen.

mcrute
quelle
Bit nitpicky, aber 'is'-Tests mit id () nur, wenn Sie keine eigene is_ () -Mitgliedsfunktion (2.3+) definiert haben. [ docs.python.org/library/operator.html]
verbrachte den
Ich nehme an, mit "überschreiben" meinen Sie tatsächlich das Patchen des Operatormoduls durch Affen. In diesem Fall ist Ihre Aussage nicht ganz richtig. Das Operatormodul wird zur Vereinfachung bereitgestellt, und das Überschreiben dieser Methoden hat keinen Einfluss auf das Verhalten des Operators "is". Ein Vergleich mit "is" verwendet immer die ID () eines Objekts für den Vergleich. Dieses Verhalten kann nicht überschrieben werden. Auch eine is_ member Funktion hat keinen Einfluss auf den Vergleich.
mcrute
mcrute - Ich habe zu früh (und falsch) gesprochen, Sie haben absolut Recht.
verbrachte den
Dies ist eine sehr schöne Lösung, insbesondere wenn die __eq__in deklariert wird CommonEqualityMixin(siehe die andere Antwort). Ich fand dies besonders nützlich beim Vergleich von Instanzen von Klassen, die von Base in SQLAlchemy abgeleitet wurden. Um nicht zu vergleichen, _sa_instance_statewechselte ich key.startswith("__")):zu key.startswith("_")):. Ich hatte auch einige Rückreferenzen in ihnen und die Antwort von Algorias erzeugte endlose Rekursion. Deshalb habe ich alle Rückreferenzen '_'so benannt, dass sie auch beim Vergleich übersprungen werden. HINWEIS: Wechseln Sie in Python 3.x iteritems()zu items().
Wookie88
@mcrute Normalerweise hat __dict__eine Instanz nichts, mit dem begonnen wird, __es sei denn, es wurde vom Benutzer definiert. Dinge wie __class__, __init__etc. sind nicht in der Instanz __dict__, sondern in seiner Klasse __dict__. OTOH, die privaten Attribute können leicht damit beginnen __und sollten wahrscheinlich für verwendet werden __eq__. Können Sie klarstellen, was genau Sie vermeiden __wollten, wenn Sie Attribute mit Präfix überspringen ?
Max
1

Anstatt Unterklassen / Mixins zu verwenden, verwende ich gerne einen generischen Klassendekorateur

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

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

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

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

Verwendungszweck:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b
bluenote10
quelle
0

Dies beinhaltet die Kommentare zu Algorias 'Antwort und vergleicht Objekte mit einem einzigen Attribut, da mir das ganze Diktat egal ist. hasattr(other, "id")muss wahr sein, aber ich weiß, dass es daran liegt, dass ich es im Konstruktor festgelegt habe.

def __eq__(self, other):
    if other is self:
        return True

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id
Noumenon
quelle