Comparar instancias de objetos para igualdad por sus atributos

244

Tengo una clase MyClass, que contiene dos variables miembro fooy bar:

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

Tengo dos instancias de esta clase, cada una de las cuales tiene valores idénticos para fooy bar:

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

Sin embargo, cuando los comparo por igualdad, Python regresa False:

>>> x == y
False

¿Cómo puedo hacer que Python considere estos dos objetos iguales?

Željko Živković
fuente

Respuestas:

355

Debe implementar el método __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

Ahora sale:

>>> x == y
True

Tenga en cuenta que la implementación __eq__automáticamente hará que las instancias de su clase no sean compartibles, lo que significa que no se pueden almacenar en conjuntos y dictados. Si no está modelando un tipo inmutable (es decir, si los atributos fooy el barvalor pueden cambiar dentro de la vida útil de su objeto), se recomienda dejar sus instancias como no compartibles.

Si está modelando un tipo inmutable, también debe implementar el enlace de modelo de datos __hash__:

class MyClass:
    ...

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

Una solución general, como la idea de recorrer __dict__y comparar valores, no es aconsejable: nunca puede ser realmente general porque __dict__puede tener tipos incomparables o no compartibles contenidos dentro.

NB: tenga en cuenta que antes de Python 3, es posible que deba usar en __cmp__lugar de __eq__. Los usuarios de Python 2 también pueden querer implementar __ne__, ya que un comportamiento predeterminado sensible para la desigualdad (es decir, invertir el resultado de igualdad) no se creará automáticamente en Python 2.

e-satis
fuente
2
Tenía curiosidad sobre el uso de return NotImplemented(en lugar de aumentar NotImplementedError). Ese tema está cubierto aquí: stackoverflow.com/questions/878943/…
init_js
48

Anula los operadores de comparación enriquecidos en su objeto.

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

Me gusta esto:

    def __eq__(self, other):
        return self._id == other._id
Christopher
fuente
3
Tenga en cuenta que en Python 2.5 y en adelante, la clase debe definir __eq__(), pero sólo uno de __lt__(), __le__(), __gt__(), o __ge__()se necesita, además de eso. A partir de eso, Python puede inferir los otros métodos. Ver functoolspara más información.
kba
1
@kba, no creo que sea verdad. Esto puede funcionar para el functoolsmódulo, pero no para comparadores estándar: MyObj1 != Myobj2solo funcionará si __ne__()se implementa el método.
Arel
66
el consejo específico sobre functools debe ser usar el @functools.total_orderingdecorador en su clase, luego, como se mencionó anteriormente, puede definir __eq__uno y otro y el resto se derivará
Anentropic
7

Implemente el __eq__método en su clase; algo como esto:

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

Editar: si desea que sus objetos se comparen igual si y solo si tienen diccionarios de instancias iguales:

def __eq__(self, other):
    return self.__dict__ == other.__dict__
Kiv
fuente
Quizás quieras self is otherver si son el mismo objeto.
S.Lott
2
-1. Incluso si se trata de dos instancias de diccionario, Python las comparará por claves / valores automáticamente. Esto no es Java ...
e-satis
La primera solución puede plantear un AttributeError. Debe insertar la línea if hasattr(other, "path") and hasattr(other, "title"):(como este buen ejemplo en la documentación de Python).
Maggyero
5

Como un resumen :

  1. Se recomienda implementar en __eq__lugar de __cmp__, excepto si ejecuta python <= 2.0 ( __eq__se ha agregado en 2.1)
  2. No se olvide de poner en práctica también __ne__(debe ser algo así como return not self.__eq__(other)o return not self == otherexceptuar caso muy especial)
  3. No olvide que el operador debe implementarse en cada clase personalizada que desee comparar (consulte el ejemplo a continuación).
  4. Si desea comparar con un objeto que puede ser Ninguno, debe implementarlo. El intérprete no puede adivinarlo ... (ver ejemplo a continuación)

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

Dependiendo de su caso específico, podría hacer:

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

Ver el diccionario Python desde los campos de un objeto

usuario1338062
fuente
También es interesante, mientras que vars devuelve un dict, el control de la unidad unitDictEqual no parece funcionar, a pesar de que la revisión visual muestra que, de hecho, son iguales. Resolví esto convirtiendo los dictados en cadenas y comparando esos: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben
2

Con Dataclasses en Python 3.7 (y superior), una comparación de instancias de objetos para igualdad es una característica incorporada.

Un backport para Dataclasses está disponible para 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
fuente
La presentación PyCon de Raymond Hettinger 2018 es una excelente manera de comenzar a usar Python Dataclasses.
Sarath Chandra
1

Al comparar instancias de objetos, __cmp__se llama a la función.

Si el operador == no funciona para usted de manera predeterminada, siempre puede redefinir la __cmp__función para el objeto.

Editar:

Como se ha señalado, la __cmp__función está en desuso desde 3.0. En su lugar, debe utilizar los métodos de "comparación rica" .

Silfverstrom
fuente
1
El CMP función es obsoleta para 3.0+
Christopher
1

Si está tratando con una o más clases que no puede cambiar desde adentro, existen formas genéricas y simples de hacerlo que tampoco dependen de una biblioteca específica de diferencia:

Método más fácil, inseguro para objetos muy complejos

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

picklees una lib de serialización muy común para los objetos de Python y, por lo tanto, podrá serializar prácticamente cualquier cosa. En el fragmento anterior, comparo el strde serializado acon el de b. A diferencia del siguiente método, este tiene la ventaja de también escribir clases personalizadas de verificación.

La mayor molestia: debido a la ordenación específica y a los métodos de codificación [de / en], picklepuede que no se obtenga el mismo resultado para objetos iguales , especialmente cuando se trata de más complejos (por ejemplo, listas de instancias de clase personalizada anidadas) como encontrará con frecuencia en algunas librerías de terceros. Para esos casos, recomendaría un enfoque diferente:

Método completo, seguro para cualquier objeto

Podría escribir una reflexión recursiva que le dará objetos serializables y luego comparar resultados

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

Ahora no importa cuáles sean sus objetos, se garantiza que la igualdad profunda funcione

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

El número de comparables tampoco importa

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

Mi caso de uso para esto fue verificar la profunda igualdad entre un conjunto diverso de modelos de Machine Learning ya entrenados dentro de las pruebas BDD. Los modelos pertenecían a un conjunto diverso de librerías de terceros. Ciertamente, implementar __eq__como otras respuestas aquí sugieren que no era una opción para mí.

Cubriendo todas las bases

Puede encontrarse en un escenario en el que una o más de las clases personalizadas que se comparan no tienen una __dict__implementación . Eso no es común, por cualquier medio, pero es el caso de un subtipo dentro clasificador Bosque aleatoria de sklearn: <type 'sklearn.tree._tree.Tree'>. Trate estas situaciones caso por caso; por ejemplo , específicamente , decidí reemplazar el contenido del tipo afectado con el contenido de un método que me proporciona información representativa sobre la instancia (en este caso, el __getstate__método). Para tal, la penúltima fila en se base_typedconvirtió en

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

Editar: por el bien de la organización, reemplacé las dos últimas líneas base_typedcon return dict_from(obj)e implementé una reflexión realmente genérica para acomodar bibliotecas más oscuras (te estoy mirando, 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()}

Tenga en cuenta que ninguno de los métodos anteriores rinde Truepara diferentes objetos con los mismos pares clave-valor pero diferentes órdenes clave / valor, como en

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

Pero si lo desea, puede utilizar de sortedantemano el método incorporado de Python .

Julio Cezar Silva
fuente
0

Escribí esto y lo coloqué en un test/utilsmódulo en mi proyecto. Para los casos en los que no es una clase, solo planifique ol 'dict, esto atravesará ambos objetos y garantizará

  1. cada atributo es igual a su contraparte
  2. No existen atributos colgantes (atributos que solo existen en un objeto)

Es grande ... no es sexy ... pero ¡oh boi, funciona!

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)

Puede limpiarlo un poco eliminando el _asserty simplemente usando ol 'simple, assertpero luego el mensaje que recibe cuando falla es muy inútil.

rayepps
fuente
0

Debe implementar el método __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
fuente
2
Por favor, editar su respuesta y añadir una explicación más detallada de su código, lo que explica por qué es diferente de los otros diez respuestas. Esta pregunta tiene diez años y ya tiene una respuesta aceptada y varias de muy alta calidad. Sin detalles adicionales, su respuesta es de una calidad mucho menor en comparación con las demás, y probablemente será rechazada o eliminada.
Das_Geek
0

A continuación funciona (en mis pruebas limitadas) haciendo una comparación profunda entre dos jerarquías de objetos. En maneja varios casos, incluidos los casos en que los objetos mismos o sus atributos son diccionarios.

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

Este es un código muy complicado, así que por favor agregue cualquier caso que no funcione para usted en los comentarios.

Shital Shah
fuente
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
fuente
-1

Si desea obtener una comparación atributo por atributo y ver si falla y dónde falla, puede utilizar la siguiente lista de comprensión:

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

La ventaja adicional aquí es que puede exprimir una línea e ingresar en la ventana "Evaluar expresión" al depurar en PyCharm.

DalyaG
fuente
-3

Intenté el ejemplo inicial (ver 7 arriba) y no funcionó en ipython. Tenga en cuenta que cmp (obj1, obj2) devuelve un "1" cuando se implementa utilizando dos instancias de objeto idénticas. Por extraño que parezca, cuando modifico uno de los valores de atributo y recompare, usando cmp (obj1, obj2) el objeto continúa devolviendo un "1". (suspiro...)

Ok, entonces lo que debes hacer es iterar dos objetos y comparar cada atributo usando el signo ==.

usuario2215595
fuente
Al menos en Python 2.7, los objetos se comparan por identidad de manera predeterminada. Eso significa para CPython en palabras prácticas que comparan por su dirección de memoria. Es por eso que cmp (o1, o2) devuelve 0 solo cuando "o1 es o2" y consistentemente 1 o -1 dependiendo de los valores de id (o1) e id (o2)
yacc143
-6

La instancia de una clase en comparación con == no es igual. La mejor manera es asignar la función cmp a su clase, que hará las cosas.

Si desea hacer una comparación por el contenido, simplemente puede usar cmp (obj1, obj2)

En su caso, cmp (doc1, doc2) devolverá -1 si el contenido es el mismo.

asb
fuente