Confronta le istanze degli oggetti per l'uguaglianza con i loro attributi


244

Ho una classe MyClass, che contiene due variabili membro fooe bar:

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

Ho due istanze di questa classe, ognuna delle quali ha valori identici per fooe bar:

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

Tuttavia, quando li confronto per uguaglianza, Python restituisce False:

>>> x == y
False

Come posso fare in modo che Python consideri questi due oggetti uguali?

Risposte:


355

È necessario implementare il metodo __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

Ora emette:

>>> x == y
True

Si noti che l'implementazione __eq__renderà automaticamente non istabili le istanze della classe, il che significa che non possono essere archiviate in set e dadi. Se non stai modellando un tipo immutabile (ad esempio se gli attributi fooe barpossono cambiare valore durante la vita del tuo oggetto), allora ti consigliamo di lasciare le tue istanze come non lavabili.

Se stai modellando un tipo immutabile, dovresti anche implementare l'hook del modello di dati __hash__:

class MyClass:
    ...

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

Una soluzione generale, come l'idea di fare il ciclo continuo __dict__e confrontare i valori, non è consigliabile: non può mai essere veramente generale perché __dict__potrebbe contenere tipi incomparabili o non lavabili contenuti all'interno.

NB: tieni presente che prima di Python 3 potresti dover usare __cmp__invece di __eq__. Gli utenti di Python 2 potrebbero anche voler implementare __ne__, poiché un comportamento predefinito ragionevole per la disuguaglianza (cioè invertire il risultato di uguaglianza) non verrà creato automaticamente in Python 2.


2
Ero curioso dell'uso di return NotImplemented(invece di aumentare NotImplementedError). Questo argomento è coperto qui: stackoverflow.com/questions/878943/...
init_js

48

Sovrascrivi i ricchi operatori di confronto nel tuo oggetto.

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

Come questo:

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

3
Si noti che in Python 2.5 e seguenti, la classe deve definire __eq__(), ma solo uno __lt__(), __le__(), __gt__(), o __ge__()è necessario in aggiunta a questo. Da ciò, Python può dedurre gli altri metodi. Vedi functoolsper maggiori informazioni.
kba

1
@kba, non penso sia vero. Questo potrebbe funzionare per il functoolsmodulo, ma non per i comparatori standard: MyObj1 != Myobj2funzionerà solo se il __ne__()metodo è implementato.
Arel,

6
il consiglio specifico su functools dovrebbe essere quello di usare il @functools.total_orderingdecoratore nella tua classe, quindi come sopra puoi definirne solo __eq__uno e l'altro e il resto verrà derivato
Anentropic

7

Implementa il __eq__metodo nella tua classe; qualcosa come questo:

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

Modifica: se vuoi che i tuoi oggetti siano uguali se e solo se hanno dizionari di istanze uguali:

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

Forse intendi self is othervedere se sono lo stesso oggetto.
S.Lott

2
-1. Anche se questa è un'istanza di due dizionari, Python li confronterà automaticamente per chiavi / valori. Questo non è Java ...
e-satis il

La prima soluzione può sollevare un AttributeError. Devi inserire la linea if hasattr(other, "path") and hasattr(other, "title"):(come questo bell'esempio nella documentazione di Python).
Maggyero,

5

Come sintesi:

  1. Si consiglia di implementare __eq__anziché __cmp__, tranne se si esegue python <= 2.0 (__eq__ è stato aggiunto in 2.1)
  2. Non dimenticare di implementare anche __ne__(dovrebbe essere qualcosa di simile return not self.__eq__(other)o return not self == othertranne un caso molto speciale)
  3. Non dimenticare che l'operatore deve essere implementato in ogni classe personalizzata che desideri confrontare (vedi esempio sotto).
  4. Se si desidera confrontare con un oggetto che può essere Nessuno, è necessario implementarlo. L'interprete non può indovinarlo ... (vedi esempio sotto)

    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)

2

A seconda del caso specifico, potresti fare:

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

Vedi il dizionario Python dai campi di un oggetto


Anche interessante, mentre vars restituisce un dict, l'asserzione unittestDictEqual non sembra funzionare, anche se la revisione visiva mostra che sono, in effetti, uguali. Ho risolto il problema trasformando i dadi in stringhe e confrontando quelli: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben

2

Con Dataclasses in Python 3.7 (e versioni successive), un confronto tra istanze di oggetto per l'uguaglianza è una funzionalità integrata.

Un backport per Dataclasses è disponibile per Python 3.6.

(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

La presentazione PyCon 2018 di Raymond Hettinger è un modo eccellente per iniziare con gli occhiali da vista Python.
Sarath Chandra,

1

Quando si confrontano istanze di oggetti, __cmp__viene chiamata la funzione.

Se l'operatore == non funziona per te per impostazione predefinita, puoi sempre ridefinire la __cmp__funzione per l'oggetto.

Modificare:

Come è stato sottolineato, la __cmp__funzione è obsoleta dal 3.0. Dovresti invece usare i metodi di "confronto avanzato" .


1
La funzione cmp è obsoleta per 3.0+
Christopher

1

Se hai a che fare con una o più classi che non puoi cambiare dall'interno, ci sono modi generici e semplici per farlo che non dipendono da una libreria specifica per diff:

Metodo più semplice, non sicuro per oggetti molto complessi

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

pickleè una lib di serializzazione molto comune per gli oggetti Python e sarà quindi in grado di serializzare praticamente qualsiasi cosa, davvero. Nel frammento sopra sto confrontando il strda serializzato acon quello di b. A differenza del metodo successivo, questo ha il vantaggio di controllare anche le classi personalizzate.

Il problema maggiore: a causa di specifici metodi di ordinamento e di codifica [de / en], picklepotrebbe non produrre lo stesso risultato per oggetti uguali , specialmente quando si tratta di più complessi (ad esempio elenchi di istanze di classe personalizzate nidificate) come spesso si trovano in alcune librerie di terze parti. Per questi casi, consiglierei un approccio diverso:

Metodo completo, sicuro per qualsiasi oggetto

Potresti scrivere una riflessione ricorsiva che ti darà oggetti serializzabili e quindi confrontare i risultati

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:])

Ora non importa quali siano i tuoi oggetti, l'eguaglianza profonda è garantita per funzionare

>>> 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

Anche il numero di elementi comparabili non ha importanza

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

Il mio caso d'uso è stato quello di verificare la profonda uguaglianza tra una serie diversificata di modelli di Machine Learning già addestrati all'interno dei test BDD. I modelli appartenevano a una serie diversificata di librerie di terze parti. Certamente implementando__eq__ come altre risposte qui suggerisce che non era un'opzione per me.

Coprendo tutte le basi

Potresti trovarti in uno scenario in cui una o più classi personalizzate confrontate non hanno __dict__un'implementazione . Questo non è comune con qualsiasi mezzo, ma è il caso di un sottotipo all'interno classificatore Foresta caso di sklearn: <type 'sklearn.tree._tree.Tree'>. Trattare queste situazioni caso per caso - ad esempio , in particolare , ho deciso di sostituire il contenuto del tipo afflitto con il contenuto di un metodo che mi fornisce informazioni rappresentative sull'istanza (in questo caso, il __getstate__metodo). Per tale motivo, la penultima riga è base_typeddiventata

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

Modifica: per motivi di organizzazione, ho sostituito le ultime due righe di base_typedcon return dict_from(obj), e ho implementato una riflessione davvero generica per accogliere librerie più oscure (ti sto guardando, 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()}

Ricorda che nessuno dei metodi sopra riportati produce Trueoggetti diversi con le stesse coppie chiave-valore ma ordini chiave / valore diversi, come in

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

Ma se lo desideri, puoi comunque utilizzare il sortedmetodo integrato di Python in anticipo.


0

L'ho scritto e inserito in un test/utilsmodulo nel mio progetto. Per i casi in cui non è una classe, basta pianificare un dict, questo attraverserà entrambi gli oggetti e garantirà

  1. ogni attributo è uguale alla sua controparte
  2. Non esistono attributi pendenti (attr esiste solo su un oggetto)

È grande ... non è sexy ... ma oh boi funziona!

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)

Puoi ripulirlo un po 'rimuovendo _asserte semplicemente usando semplicemente il vecchio assertma poi il messaggio che ricevi quando fallisce è molto inutile.


0

È necessario implementare il metodo __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

2
Si prega di modificare la tua risposta e aggiungere ulteriori spiegazioni al codice, spiegando perché è diverso dagli altri dieci risposte. Questa domanda ha dieci anni e ha già una risposta accettata e molte altre di altissima qualità. Senza ulteriori dettagli, la tua risposta è di qualità molto più bassa rispetto alle altre e molto probabilmente verrà ridimensionata o eliminata.
Das_Geek,

0

Di seguito funziona (nei miei test limitati) facendo un confronto profondo tra due gerarchie di oggetti. In gestisce vari casi, inclusi i casi in cui gli oggetti stessi oi loro attributi sono dizionari.

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

Questo è un codice molto complicato, quindi per favore aggiungi eventuali casi che potrebbero non funzionare nei tuoi commenti.


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

-1

Se vuoi ottenere un confronto attributo per attributo e vedere se e dove fallisce, puoi usare la seguente comprensione dell'elenco:

[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]

Il vantaggio extra qui è che puoi comprimerlo di una riga ed entrare nella finestra "Valuta espressione" durante il debug in PyCharm.


-3

Ho provato l'esempio iniziale (vedi 7 sopra) e non ha funzionato in ipython. Si noti che cmp (obj1, obj2) restituisce un "1" quando implementato utilizzando due istanze di oggetto identiche. Abbastanza stranamente quando modifico uno dei valori dell'attributo e ricompenso, usando cmp (obj1, obj2) l'oggetto continua a restituire un "1". (sospiro...)

Ok, quindi quello che devi fare è iterare due oggetti e confrontare ogni attributo usando il segno ==.


Almeno in Python 2.7, gli oggetti vengono confrontati per identità per impostazione predefinita. Ciò significa che per CPython, in termini pratici, si confrontano per indirizzo di memoria. Ecco perché cmp (o1, o2) restituisce 0 solo quando "o1 è o2" e costantemente 1 o -1 a seconda dei valori di id (o1) e id (o2)
yacc143

-6

L'istanza di una classe rispetto a == non è uguale. Il modo migliore è quello di fornire la funzione cmp alla tua classe che farà le cose.

Se vuoi fare un confronto in base al contenuto, puoi semplicemente usare cmp (obj1, obj2)

Nel tuo caso cmp (doc1, doc2) Restituirà -1 se il contenuto è lo stesso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.