Vergleichen Sie Objektinstanzen auf Gleichheit anhand ihrer Attribute

243

Ich habe eine Klasse MyClass, die zwei Mitgliedsvariablen enthält foound bar:

class MyClass:
    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar

Ich habe zwei Instanzen dieser Klasse, von denen jede identische Werte für foound hat bar:

x = MyClass('foo', 'bar')
y = MyClass('foo', 'bar')

Wenn ich sie jedoch auf Gleichheit vergleiche, gibt Python Folgendes zurück False:

>>> x == y
False

Wie kann ich Python dazu bringen, diese beiden Objekte als gleich zu betrachten?

Željko Živković
quelle

Antworten:

353

Sie sollten die Methode implementieren __eq__:

class MyClass:
    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar

    def __eq__(self, other): 
        if not isinstance(other, MyClass):
            # don't attempt to compare against unrelated types
            return NotImplemented

        return self.foo == other.foo and self.bar == other.bar

Jetzt gibt es aus:

>>> x == y
True

Beachten Sie, dass durch die Implementierung __eq__Instanzen Ihrer Klasse automatisch nicht mehr verwertbar sind. Dies bedeutet, dass sie nicht in Mengen und Diktaten gespeichert werden können. Wenn Sie keinen unveränderlichen Typ modellieren (dh wenn die Attribute fooundbar Wert innerhalb der Lebensdauer Ihres Objekts ändern), wird empfohlen, Ihre Instanzen einfach als nicht verwertbar zu belassen.

Wenn Sie einen unveränderlichen Typ modellieren, sollten Sie auch den Datenmodell-Hook implementieren __hash__:

class MyClass:
    ...

    def __hash__(self):
        # necessary for instances to behave sanely in dicts and sets.
        return hash((self.foo, self.bar))

Eine allgemeine Lösung, wie die Idee __dict__, Werte zu durchlaufen und zu vergleichen, ist nicht ratsam - sie kann niemals wirklich allgemein sein, weil die__dict__ möglicherweise unvergleichliche oder nicht verwischbare Typen enthält.

NB: Beachten Sie, dass Sie vor Python 3 möglicherweise __cmp__anstelle von verwenden müssen __eq__. Python 2-Benutzer möchten möglicherweise auch implementieren __ne__, da ein sinnvolles Standardverhalten für Ungleichheit (dh das Invertieren des Gleichheitsergebnisses) in Python 2 nicht automatisch erstellt wird.

e-satis
quelle
2
Ich war neugierig auf die Verwendung von return NotImplemented(anstatt zu erhöhen NotImplementedError). Dieses Thema wird hier behandelt: stackoverflow.com/questions/878943/…
init_js
48

Sie überschreiben die umfangreichen Vergleichsoperatoren in Ihrem Objekt.

class MyClass:
 def __lt__(self, other):
      # return comparison
 def __le__(self, other):
      # return comparison
 def __eq__(self, other):
      # return comparison
 def __ne__(self, other):
      # return comparison
 def __gt__(self, other):
      # return comparison
 def __ge__(self, other):
      # return comparison

So was:

    def __eq__(self, other):
        return self._id == other._id
Christopher
quelle
3
Beachten Sie, dass in Python 2.5 und weiter muss die Klasse definieren __eq__(), aber nur einer von __lt__(), __le__(), __gt__(), oder __ge__()ist zusätzlich zu dem Bedarf. Daraus kann Python die anderen Methoden ableiten. Siehe functoolsfür weitere Informationen.
kba
1
@kba, ich glaube nicht, dass das stimmt. Dies funktioniert möglicherweise für das functoolsModul, funktioniert jedoch nicht für Standardkomparatoren: Funktioniert MyObj1 != Myobj2nur, wenn die __ne__()Methode implementiert ist.
Arel
6
Der spezielle Tipp zu functools sollte sein, den @functools.total_orderingDekorator für Ihre Klasse zu verwenden. Dann können Sie wie oben nur einen __eq__und einen anderen definieren, und der Rest wird abgeleitet
Anentropic
7

Implementieren Sie die __eq__Methode in Ihrer Klasse. etwas wie das:

def __eq__(self, other):
    return self.path == other.path and self.title == other.title

Bearbeiten: Wenn Sie möchten, dass Ihre Objekte genau dann gleich verglichen werden, wenn sie gleiche Instanzwörterbücher haben:

def __eq__(self, other):
    return self.__dict__ == other.__dict__
Kiv
quelle
Vielleicht wollen Sie self is othersehen, ob es sich um dasselbe Objekt handelt.
S.Lott
2
-1. Selbst wenn dies zwei Wörterbuchinstanzen sind, vergleicht Python sie automatisch anhand von Schlüsseln / Werten. Dies ist nicht Java ...
e-satis
Die erste Lösung kann eine auslösen AttributeError. Sie müssen die Zeile einfügen if hasattr(other, "path") and hasattr(other, "title"):(wie dieses schöne Beispiel in der Python-Dokumentation).
Maggyero
5

Zusammenfassend:

  1. Es wird empfohlen, nicht zu implementieren, es __eq__sei denn __cmp__, Sie führen Python <= 2.0 aus (__eq__ wurde in 2.1 hinzugefügt).
  2. Vergessen Sie nicht, auch zu implementieren __ne__(sollte so etwas wie return not self.__eq__(other)oder return not self == otheraußer einem ganz besonderen Fall sein)
  3. Vergessen Sie nicht, dass der Operator in jeder benutzerdefinierten Klasse implementiert sein muss, die Sie vergleichen möchten (siehe Beispiel unten).
  4. Wenn Sie mit einem Objekt vergleichen möchten, das None sein kann, müssen Sie es implementieren. Der Dolmetscher kann es nicht erraten ... (siehe Beispiel unten)

    class B(object):
      def __init__(self):
        self.name = "toto"
      def __eq__(self, other):
        if other is None:
          return False
        return self.name == other.name
    
    class A(object):
      def __init__(self):
        self.toto = "titi"
        self.b_inst = B()
      def __eq__(self, other):
        if other is None:
          return False
        return (self.toto, self.b_inst) == (other.toto, other.b_inst)
fievel
quelle
2

Abhängig von Ihrem speziellen Fall können Sie Folgendes tun:

>>> vars(x) == vars(y)
True

Siehe Python-Wörterbuch aus den Feldern eines Objekts

user1338062
quelle
Auch interessant, obwohl vars ein Diktat zurückgibt, scheint das assertDictEqual von unittest nicht zu funktionieren, obwohl die visuelle Überprüfung zeigt, dass sie tatsächlich gleich sind. Ich habe das umgangen, indem ich die Diktate in Strings umgewandelt und diese verglichen habe: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0))
Ben
2

Mit Datenklassen in Python 3.7 (und höher) ist ein Vergleich von Objektinstanzen auf Gleichheit eine integrierte Funktion.

Für Python 3.6 ist ein Backport für Datenklassen verfügbar.

(Py37) nsc@nsc-vbox:~$ python
Python 3.7.5 (default, Nov  7 2019, 10:50:52) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dataclasses import dataclass
>>> @dataclass
... class MyClass():
...     foo: str
...     bar: str
... 
>>> x = MyClass(foo="foo", bar="bar")
>>> y = MyClass(foo="foo", bar="bar")
>>> x == y
True
Sarath Chandra
quelle
Die PyCon-Präsentation 2018 von Raymond Hettinger ist eine hervorragende Möglichkeit, um mit Python-Datenklassen zu beginnen.
Sarath Chandra
1

Beim Vergleichen von Objektinstanzen wird die __cmp__Funktion aufgerufen.

Wenn der Operator == standardmäßig nicht für Sie __cmp__funktioniert , können Sie die Funktion für das Objekt jederzeit neu definieren .

Bearbeiten:

Wie bereits erwähnt, ist die __cmp__Funktion seit 3.0 veraltet. Stattdessen sollten Sie die Methoden "Rich Compare" verwenden.

Silfverstrom
quelle
1
Die cmp- Funktion ist für 3.0+
Christopher
0

Ich habe das geschrieben und in ein test/utilsModul in meinem Projekt eingefügt. In Fällen, in denen es sich nicht um eine Klasse handelt, planen Sie einfach das alte Diktat. Dadurch werden beide Objekte durchlaufen und sichergestellt

  1. Jedes Attribut ist gleich seinem Gegenstück
  2. Es sind keine baumelnden Attribute vorhanden (Attribute, die nur für ein Objekt vorhanden sind).

Es ist groß ... es ist nicht sexy ... aber oh Boi, es funktioniert!

def assertObjectsEqual(obj_a, obj_b):

    def _assert(a, b):
        if a == b:
            return
        raise AssertionError(f'{a} !== {b} inside assertObjectsEqual')

    def _check(a, b):
        if a is None or b is None:
            _assert(a, b)
        for k,v in a.items():
            if isinstance(v, dict):
                assertObjectsEqual(v, b[k])
            else:
                _assert(v, b[k])

    # Asserting both directions is more work
    # but it ensures no dangling values on
    # on either object
    _check(obj_a, obj_b)
    _check(obj_b, obj_a)

Sie können es ein wenig aufräumen, indem Sie das entfernen _assertund einfach nur ol 'verwenden, assertaber dann ist die Meldung, die Sie erhalten, wenn es fehlschlägt, sehr wenig hilfreich.

Rayepps
quelle
0

Sie sollten die Methode implementieren __eq__:

 class MyClass:
      def __init__(self, foo, bar, name):
           self.foo = foo
           self.bar = bar
           self.name = name

      def __eq__(self,other):
           if not isinstance(other,MyClass):
                return NotImplemented
           else:
                #string lists of all method names and properties of each of these objects
                prop_names1 = list(self.__dict__)
                prop_names2 = list(other.__dict__)

                n = len(prop_names1) #number of properties
                for i in range(n):
                     if getattr(self,prop_names1[i]) != getattr(other,prop_names2[i]):
                          return False

                return True
Victor H. De Oliveira Côrtes
quelle
2
Bitte bearbeiten Sie Ihre Antwort und fügen Sie Ihrem Code weitere Erklärungen hinzu, um zu erklären, warum er sich von den zehn anderen Antworten unterscheidet. Diese Frage ist zehn Jahre alt und hat bereits eine akzeptierte Antwort und mehrere sehr hochwertige. Ohne zusätzliche Details ist Ihre Antwort im Vergleich zu den anderen von viel geringerer Qualität und wird höchstwahrscheinlich herabgestuft oder gelöscht.
Das_Geek
0

Das Folgende funktioniert (in meinen begrenzten Tests) durch einen tiefen Vergleich zwischen zwei Objekthierarchien. In behandelt verschiedene Fälle, einschließlich der Fälle, in denen Objekte selbst oder ihre Attribute Wörterbücher sind.

def deep_comp(o1:Any, o2:Any)->bool:
    # NOTE: dict don't have __dict__
    o1d = getattr(o1, '__dict__', None)
    o2d = getattr(o2, '__dict__', None)

    # if both are objects
    if o1d is not None and o2d is not None:
        # we will compare their dictionaries
        o1, o2 = o1.__dict__, o2.__dict__

    if o1 is not None and o2 is not None:
        # if both are dictionaries, we will compare each key
        if isinstance(o1, dict) and isinstance(o2, dict):
            for k in set().union(o1.keys() ,o2.keys()):
                if k in o1 and k in o2:
                    if not deep_comp(o1[k], o2[k]):
                        return False
                else:
                    return False # some key missing
            return True
    # mismatched object types or both are scalers, or one or both None
    return o1 == o2

Dies ist ein sehr kniffliger Code. Fügen Sie daher in Kommentaren Fälle hinzu, die für Sie möglicherweise nicht funktionieren.

Shital Shah
quelle
0
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

    def __repr__(self):
        return str(self.value)

    def __eq__(self,other):
        return self.value == other.value

node1 = Node(1)
node2 = Node(1)

print(f'node1 id:{id(node1)}')
print(f'node2 id:{id(node2)}')
print(node1 == node2)
>>> node1 id:4396696848
>>> node2 id:4396698000
>>> True
Tomgtbst
quelle
0

Wenn Sie es mit einer oder mehreren Klassen zu tun haben, die Sie von innen nicht ändern können , gibt es allgemeine und einfache Möglichkeiten, die auch nicht von einer diff-spezifischen Bibliothek abhängen:

Einfachste, für sehr komplexe Objekte unsichere Methode

pickle.dumps(a) == pickle.dumps(b)

pickleist eine sehr verbreitete Serialisierungsbibliothek für Python-Objekte und kann daher so ziemlich alles serialisieren. Im obigen Snippet vergleiche ich das strvon serialisiert amit dem vonb . Im Gegensatz zur nächsten Methode hat diese den Vorteil, dass auch benutzerdefinierte Klassen auf Typprüfung überprüft werden können.

Der größte Aufwand: Aufgrund spezifischer Ordnungs- und [de / en] -Codierungsmethoden wird picklemöglicherweise nicht das gleiche Ergebnis für gleiche Objekte erzielt , insbesondere bei komplexeren Objekten (z. B. Listen verschachtelter Instanzen benutzerdefinierter Klassen), wie Sie sie häufig finden in einigen Bibliotheken von Drittanbietern. Für diese Fälle würde ich einen anderen Ansatz empfehlen:

Gründliche, für jedes Objekt sichere Methode

Sie können eine rekursive Reflexion schreiben, die Ihnen serialisierbare Objekte liefert, und dann die Ergebnisse vergleichen

from collections.abc import Iterable

BASE_TYPES = [str, int, float, bool, type(None)]


def base_typed(obj):
    """Recursive reflection method to convert any object property into a comparable form.
    """
    T = type(obj)
    from_numpy = T.__module__ == 'numpy'

    if T in BASE_TYPES or callable(obj) or (from_numpy and not isinstance(T, Iterable)):
        return obj

    if isinstance(obj, Iterable):
        base_items = [base_typed(item) for item in obj]
        return base_items if from_numpy else T(base_items)

    d = obj if T is dict else obj.__dict__

    return {k: base_typed(v) for k, v in d.items()}


def deep_equals(*args):
    return all(base_typed(args[0]) == base_typed(other) for other in args[1:])

Jetzt spielt es keine Rolle, was Ihre Objekte sind, tiefe Gleichheit ist sicher zu funktionieren

>>> from sklearn.ensemble import RandomForestClassifier
>>>
>>> a = RandomForestClassifier(max_depth=2, random_state=42)
>>> b = RandomForestClassifier(max_depth=2, random_state=42)
>>> 
>>> deep_equals(a, b)
True

Die Anzahl der Vergleiche spielt ebenfalls keine Rolle

>>> c = RandomForestClassifier(max_depth=2, random_state=1000)
>>> deep_equals(a, b, c)
False

Mein Anwendungsfall hierfür war die Überprüfung der tiefen Gleichheit zwischen einer Vielzahl bereits trainierter Modelle für maschinelles Lernen in BDD-Tests. Die Modelle gehörten zu einer Vielzahl von Bibliotheken von Drittanbietern. __eq__Wie andere Antworten hier zu implementieren , war für mich sicherlich keine Option.

Bedeckt alle Basen

Möglicherweise befinden Sie sich in einem Szenario, in dem eine oder mehrere der verglichenen benutzerdefinierten Klassen keine __dict__Implementierung haben . Das ist keineswegs üblich, aber es handelt sich um einen Subtyp innerhalb des Random Forest-Klassifikators von sklearn : <type 'sklearn.tree._tree.Tree'>. Behandeln Sie diese Situationen in einem Einzelfall - zum Beispiel speziell , ich den Inhalt des betroffenen Typs mit dem Inhalt einer Methode zu ersetzen , entschieden , die mir repräsentative Informationen über die Instanz (in diesem Fall das gibt __getstate__Methode). Für eine solche, in der zweiten bis letzten Zeile base_typedwurde

d = obj if T is dict else obj.__dict__ if '__dict__' in dir(obj) else obj.__getstate__()

Bearbeiten: Aus Gründen der Organisation habe ich die letzten beiden Zeilen base_typeddurch ersetzt return dict_from(obj)und eine wirklich allgemeine Reflexion implementiert, um dunkelere Bibliotheken aufzunehmen (ich sehe dich an, Doc2Vec).

def isproperty(prop, obj):
    return not callable(getattr(obj, prop)) and not prop.startswith('_')


def dict_from(obj):
    """Converts dict-like objects into dicts
    """
    if isinstance(obj, dict):
        # Dict and subtypes are directly converted
        d = dict(obj)

    elif '__dict__' in dir(obj):
        d = obj.__dict__

    elif str(type(obj)) == 'sklearn.tree._tree.Tree':
        # Replaces sklearn trees with their state metadata
        d = obj.__getstate__()

    else:
        # Extract non-callable, non-private attributes with reflection
        kv = [(p, getattr(obj, p)) for p in dir(obj) if isproperty(p, obj)]
        d = {k: v for k, v in kv}

    return {k: base_typed(v) for k, v in d.items()}

Beachten Sie, dass keine der oben genannten Methoden Truefür verschiedene Objekte mit denselben Schlüssel-Wert-Paaren, aber unterschiedlichen Schlüssel / Wert-Reihenfolgen wie in ergibt

>>> a = {'foo':[], 'bar':{}}
>>> b = {'bar':{}, 'foo':[]}
>>> pickle.dumps(a) == pickle.dumps(b)
False

Wenn Sie dies jedoch möchten, können Sie die integrierte sortedMethode von Python ohnehin vorher verwenden.

Julio Cezar Silva
quelle
-1

Wenn Sie einen Attribut-für-Attribut-Vergleich durchführen möchten und feststellen möchten, ob und wo dies fehlschlägt, können Sie das folgende Listenverständnis verwenden:

[i for i,j in 
 zip([getattr(obj_1, attr) for attr in dir(obj_1)],
     [getattr(obj_2, attr) for attr in dir(obj_2)]) 
 if not i==j]

Der zusätzliche Vorteil hierbei ist, dass Sie beim Debuggen in PyCharm eine Zeile zusammendrücken und in das Fenster "Ausdruck auswerten" eingeben können.

DalyaG
quelle
-3

Ich habe das erste Beispiel ausprobiert (siehe 7 oben) und es hat in ipython nicht funktioniert. Beachten Sie, dass cmp (obj1, obj2) eine "1" zurückgibt, wenn es mit zwei identischen Objektinstanzen implementiert wird. Seltsamerweise gibt das Objekt weiterhin eine "1" zurück, wenn ich einen der Attributwerte ändere und mit cmp (obj1, obj2) neu vergleiche. (Seufzer...)

Ok, Sie müssen also zwei Objekte iterieren und jedes Attribut mit dem Zeichen == vergleichen.

user2215595
quelle
Zumindest in Python 2.7 werden Objekte standardmäßig nach Identität verglichen. Das bedeutet für CPython in praktischen Worten, dass sie anhand ihrer Speicheradresse vergleichen. Deshalb gibt cmp (o1, o2) nur dann 0 zurück, wenn "o1 ist o2" und konsistent 1 oder -1, abhängig von den Werten von id (o1) und id (o2)
yacc143
-6

Die Instanz einer Klasse im Vergleich zu == ist ungleich. Der beste Weg ist, den cmp zu ass Funktion Ihrer Klasse , die das Zeug .

Wenn Sie den Inhalt vergleichen möchten, können Sie einfach cmp (obj1, obj2) verwenden.

In Ihrem Fall cmp (doc1, doc2) Es wird -1 zurückgegeben, wenn der Inhalt identisch ist.

asb
quelle