Comparer les instances d'objets pour l'égalité par leurs attributs

244

J'ai une classe MyClass, qui contient deux variables membres fooet bar:

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

J'ai deux instances de cette classe, chacune ayant des valeurs identiques pour fooet bar:

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

Cependant, quand je les compare pour l'égalité, Python retourne False:

>>> x == y
False

Comment puis-je faire en sorte que python considère ces deux objets comme égaux?

Željko Živković
la source

Réponses:

355

Vous devez implémenter la méthode __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

Maintenant, il génère:

>>> x == y
True

Notez que l'implémentation __eq__rendra automatiquement les instances de votre classe inutilisables, ce qui signifie qu'elles ne peuvent pas être stockées dans des ensembles et des dict. Si vous ne modélisez pas un type immuable (c'est-à-dire si les attributs fooet barpeuvent changer de valeur pendant la durée de vie de votre objet), il est recommandé de laisser vos instances comme non partageables.

Si vous modélisez un type immuable, vous devez également implémenter le hook de modèle de données __hash__:

class MyClass:
    ...

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

Une solution générale, comme l'idée de parcourir __dict__et de comparer des valeurs, n'est pas recommandée - elle ne peut jamais être vraiment générale car elles __dict__peuvent contenir des types incomparables ou inchangeables.

NB: sachez qu'avant Python 3, vous devrez peut-être utiliser à la __cmp__place de __eq__. Les utilisateurs de Python 2 peuvent également vouloir implémenter __ne__, car un comportement par défaut sensible pour l'inégalité (c'est-à-dire inverser le résultat d'égalité) ne sera pas créé automatiquement dans Python 2.

e-satis
la source
2
J'étais curieux de savoir utiliser return NotImplemented(au lieu d'élever NotImplementedError). Ce sujet est couvert ici: stackoverflow.com/questions/878943/…
init_js
48

Vous remplacez les opérateurs de comparaison riches dans votre objet.

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

Comme ça:

    def __eq__(self, other):
        return self._id == other._id
Christophe
la source
3
Notez que dans Python 2.5 et au- delà, la classe doit définir __eq__(), mais seulement l' un des __lt__(), __le__(), __gt__()ou __ge__()est nécessaire en plus de cela. De cela, Python peut déduire les autres méthodes. Voir functoolspour plus d'informations.
kba
1
@kba, je ne pense pas que ce soit vrai. Cela peut fonctionner pour le functoolsmodule, mais ne fonctionne pas pour les comparateurs standard: MyObj1 != Myobj2ne fonctionnera que si la __ne__()méthode est implémentée.
Arel
6
l'astuce spécifique sur functools devrait être d'utiliser le @functools.total_orderingdécorateur sur votre classe, alors comme ci-dessus, vous pouvez définir juste __eq__et un autre et le reste sera dérivé
Anentropic
7

Implémentez la __eq__méthode dans votre classe; quelque chose comme ça:

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

Modifier: si vous voulez que vos objets soient égaux si et seulement s'ils ont des dictionnaires d'instance égaux:

def __eq__(self, other):
    return self.__dict__ == other.__dict__
Kiv
la source
Vous voulez peut-être self is othervoir s'ils sont le même objet.
S.Lott
2
-1. Même s'il s'agit de deux instances de dictionnaire, Python les comparera automatiquement par clés / valeurs. Ce n'est pas Java ...
e-satis
La première solution peut soulever un AttributeError. Vous devez insérer la ligne if hasattr(other, "path") and hasattr(other, "title"):(comme ce bel exemple dans la documentation Python).
Maggyero
5

En résumé:

  1. Il est conseillé d'implémenter __eq__plutôt que __cmp__, sauf si vous exécutez python <= 2.0 ( __eq__a été ajouté en 2.1)
  2. N'oubliez pas d'implémenter également __ne__(devrait être quelque chose comme return not self.__eq__(other)ou return not self == othersauf cas très spécial)
  3. N'oubliez pas que l'opérateur doit être implémenté dans chaque classe personnalisée que vous souhaitez comparer (voir l'exemple ci-dessous).
  4. Si vous souhaitez comparer avec un objet qui peut être Aucun, vous devez l'implémenter. L'interprète ne peut pas le deviner ... (voir l'exemple ci-dessous)

    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
la source
2

Selon votre cas spécifique, vous pourriez faire:

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

Voir le dictionnaire Python à partir des champs d'un objet

user1338062
la source
Aussi intéressant, alors que vars renvoie un dict, assertDictEqual de unittest ne semble pas fonctionner, même si l'examen visuel montre qu'ils sont, en fait, égaux. J'ai contourné cela en transformant les dict en chaînes et en les comparant: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben
2

Avec des Dataclasses dans Python 3.7 (et supérieur), une comparaison des instances d'objet pour l'égalité est une fonctionnalité intégrée.

Un backport pour Dataclasses est disponible pour 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
Sarath Chandra
la source
La présentation PyCon 2018 de Raymond Hettinger est un excellent moyen de commencer avec les classes de données Python.
Sarath Chandra
1

Lorsque vous comparez des instances d'objets, le __cmp__ fonction est appelée.

Si l'opérateur == ne fonctionne pas pour vous par défaut, vous pouvez toujours redéfinir le __cmp__ fonction de l'objet.

Éditer:

Comme cela a été souligné, la __cmp__fonction est obsolète depuis la version 3.0. Au lieu de cela, vous devez utiliser les méthodes de «comparaison riche» .

Silfverstrom
la source
1
La fonction cmp est déconseillée pour la version 3.0+
Christopher
1

Si vous avez affaire à une ou plusieurs classes que vous ne pouvez pas modifier de l'intérieur, il existe des moyens génériques et simples de le faire qui ne dépendent pas non plus d'une bibliothèque spécifique aux différences:

Méthode la plus simple et peu sûre pour les objets très complexes

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

pickleest une bibliothèque de sérialisation très courante pour les objets Python, et sera donc capable de sérialiser à peu près n'importe quoi, vraiment. Dans l'extrait ci-dessus, je compare le strde sérialisé aà celui de b. Contrairement à la méthode suivante, celle-ci présente également l'avantage de vérifier les classes personnalisées.

Le plus gros problème: en raison de méthodes de classement et de codage [de / en] spécifiques, il se picklepeut que le résultat ne soit pas le même pour des objets égaux , en particulier lorsqu'il s'agit d' plus complexes (par exemple, des listes d'instances de classe personnalisée imbriquées) comme vous en trouverez fréquemment dans certaines bibliothèques tierces. Pour ces cas, je recommanderais une approche différente:

Méthode complète et sûre pour tout objet

Vous pouvez écrire une réflexion récursive qui vous donnera des objets sérialisables, puis comparer les résultats

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

Peu importe ce que sont vos objets, une égalité profonde est assurée pour fonctionner

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

Peu importe le nombre de comparables

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

Mon cas d'utilisation était de vérifier une profonde égalité parmi un ensemble diversifié de modèles d'apprentissage automatique déjà formés dans les tests BDD. Les modèles appartenaient à un ensemble diversifié de bibliothèques tierces. La mise en œuvre __eq__comme les autres réponses suggèrent certainement que ce n'était pas une option pour moi.

Couvrant toutes les bases

Vous pouvez être dans un scénario où une ou plusieurs des classes personnalisées comparées n'ont pas d' __dict__implémentation . Ce n'est pas commun par tous les moyens, mais il est le cas d'un sous - type au sein du classificateur Forêt aléatoire de sklearn: <type 'sklearn.tree._tree.Tree'>. Traitez ces situations au cas par cas - par exemple, spécifiquement , j'ai décidé de remplacer le contenu du type affecté par le contenu d'une méthode qui me donne des informations représentatives sur l'instance (dans ce cas, la __getstate__méthode). Pour tel, l'avant-dernier rang base_typedest devenu

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

Edit: pour des raisons d'organisation, j'ai remplacé les deux dernières lignes de base_typedwith return dict_from(obj), et implémenté une réflexion vraiment générique pour accueillir des bibliothèques plus obscures (je vous regarde, 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()}

N'oubliez pas qu'aucune des méthodes ci-dessus ne donne Truepour différents objets avec les mêmes paires clé-valeur mais des ordres clé / valeur différents, comme dans

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

Mais si vous le souhaitez, vous pouvez de toute façon utiliser la sortedméthode intégrée de Python au préalable.

Julio Cezar Silva
la source
0

J'ai écrit cela et l'ai placé dans un test/utilsmodule de mon projet. Pour les cas où ce n'est pas une classe, il suffit de planifier ol 'dict, cela traversera les deux objets et assurera

  1. chaque attribut est égal à son homologue
  2. Il n'existe aucun attribut pendant (attrs qui n'existent que sur un seul objet)

C'est gros ... ce n'est pas sexy ... mais oh boi ça marche!

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)

Vous pouvez le nettoyer un peu en supprimant le _assertet en utilisant simplement ol ' assertmais le message que vous obtenez en cas d'échec est très inutile.

rayepps
la source
0

Vous devez implémenter la méthode __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
la source
2
Veuillez modifier votre réponse et ajouter des explications supplémentaires à votre code, en expliquant pourquoi il est différent des dix autres réponses. Cette question a dix ans et a déjà une réponse acceptée et plusieurs réponses de très haute qualité. Sans détails supplémentaires, votre réponse est de bien meilleure qualité que les autres et sera très probablement déclassée ou supprimée.
Das_Geek
0

Ci-dessous fonctionne (dans mes tests limités) en effectuant une comparaison approfondie entre deux hiérarchies d'objets. Dans gère divers cas, y compris les cas où les objets eux-mêmes ou leurs attributs sont des dictionnaires.

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

Il s'agit d'un code très délicat, veuillez donc ajouter les cas qui pourraient ne pas fonctionner pour vous dans les commentaires.

Shital Shah
la source
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
la source
-1

Si vous souhaitez obtenir une comparaison attribut par attribut et voir si et où cela échoue, vous pouvez utiliser la compréhension de liste suivante:

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

L'avantage supplémentaire ici est que vous pouvez presser une ligne et entrer dans la fenêtre "Evaluer l'expression" lors du débogage dans PyCharm.

DalyaG
la source
-3

J'ai essayé l'exemple initial (voir 7 ci-dessus) et cela n'a pas fonctionné en ipython. Notez que cmp (obj1, obj2) renvoie un "1" lorsqu'il est implémenté à l'aide de deux instances d'objet identiques. Curieusement, lorsque je modifie l'une des valeurs d'attribut et que je la compare, en utilisant cmp (obj1, obj2), l'objet continue de renvoyer un "1". (soupir...)

Ok, donc ce que vous devez faire est d'itérer deux objets et de comparer chaque attribut en utilisant le signe ==.

user2215595
la source
Dans Python 2.7 au moins, les objets sont comparés par identité par défaut. Cela signifie pour CPython en termes pratiques qu'ils comparent par leur adresse mémoire. C'est pourquoi cmp (o1, o2) renvoie 0 uniquement lorsque "o1 est o2" et systématiquement 1 ou -1 selon les valeurs de id (o1) et id (o2)
yacc143
-6

L'instance d'une classe par rapport à == devient non égale. La meilleure façon est d' assigner la fonction cmp à votre classe qui fera le travail.

Si vous voulez faire une comparaison par le contenu, vous pouvez simplement utiliser cmp (obj1, obj2)

Dans votre cas cmp (doc1, doc2) Il retournera -1 si le contenu est le même.

asb
la source