Compare instâncias de objeto para igualdade por seus atributos

243

Eu tenho uma classe MyClass, que contém duas variáveis ​​de membro fooe bar:

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

Eu tenho duas instâncias dessa classe, cada uma com valores idênticos para fooe bar:

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

No entanto, quando os comparo por igualdade, o Python retorna False:

>>> x == y
False

Como posso fazer python considerar esses dois objetos iguais?

Željko Živković
fonte

Respostas:

353

Você deve implementar o 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

Agora ele gera:

>>> x == y
True

Observe que a implementação __eq__tornará automaticamente instâncias da sua classe unhashable, o que significa que elas não podem ser armazenadas em sets e dict. Se você não está modelando um tipo imutável (ou seja, se os atributos fooe barpodem alterar o valor durante a vida útil do seu objeto), é recomendável deixar suas instâncias como removíveis.

Se você estiver modelando um tipo imutável, também deverá implementar o gancho do modelo de dados __hash__:

class MyClass:
    ...

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

Uma solução geral, como a idéia de repetir __dict__e comparar valores, não é aconselhável - ela nunca pode ser verdadeiramente geral porque __dict__pode ter tipos incomparáveis ​​ou removíveis de unhas contidos nela.

Nota: esteja ciente de que antes do Python 3, você pode precisar usar em __cmp__vez de __eq__. Os usuários do Python 2 também podem querer implementar __ne__, pois um comportamento padrão sensível à desigualdade (ou seja, inverter o resultado da igualdade) não será criado automaticamente no Python 2.

e-satis
fonte
2
Eu estava curioso sobre o uso de return NotImplemented(em vez de aumentar NotImplementedError). Esse tópico é abordado aqui: stackoverflow.com/questions/878943/…
init_js 31/12/19
48

Você substitui os ricos operadores de comparação em seu 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

Como isso:

    def __eq__(self, other):
        return self._id == other._id
Christopher
fonte
3
Note-se que em Python 2,5 e em diante, a classe deve definir __eq__(), mas apenas um de __lt__(), __le__(), __gt__(), ou __ge__()é necessário, em adição a isso. A partir disso, o Python pode inferir os outros métodos. Veja functoolspara mais informações.
Kba
1
@ kba, não acho que seja verdade. Isso pode funcionar para o functoolsmódulo, mas não para comparadores padrão: MyObj1 != Myobj2só funcionará se o __ne__()método for implementado.
Arel
6
a ponta específicas sobre functools deve ser para usar o @functools.total_orderingdecorador em sua classe, em seguida, como acima você pode definir apenas __eq__e um outro eo resto será derivado
Anentropic
7

Implemente o __eq__método em sua classe; algo assim:

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

Editar: se você deseja que seus objetos sejam comparados iguais se e somente se tiverem dicionários de instância iguais:

def __eq__(self, other):
    return self.__dict__ == other.__dict__
Kiv
fonte
Talvez você queira self is otherver se eles são o mesmo objeto.
S.Lott
2
-1. Mesmo que essa seja uma instância de dois dicionários, o Python os comparará por chaves / valores automaticamente. Isto não é Java ...
e-satis
A primeira solução pode gerar um AttributeError. Você precisa inserir a linha if hasattr(other, "path") and hasattr(other, "title"):(como este belo exemplo na documentação do Python).
Maggyero
5

Como resumo:

  1. É recomendável implementar em __eq__vez de __cmp__, exceto se você executar o python <= 2.0 ( __eq__foi adicionado no 2.1)
  2. Não se esqueça de implementar também __ne__(deve ser algo como return not self.__eq__(other)ou return not self == otherexceto caso muito especial)
  3. Não esqueça que o operador deve ser implementado em cada classe personalizada que você deseja comparar (veja o exemplo abaixo).
  4. Se você deseja comparar com o objeto que pode ser Nenhum, você deve implementá-lo. O intérprete não consegue adivinhar ... (veja o exemplo abaixo)

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

Dependendo do seu caso específico, você pode fazer:

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

Veja o dicionário Python a partir dos campos de um objeto

user1338062
fonte
Também interessante, enquanto vars retorna um ditado, o assertDictEqual da unittest parece não funcionar, embora a revisão visual mostre que eles são, de fato, iguais. Eu resolvi isso transformando os dictos em strings e comparando-os: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben
2

Com as classes de dados no Python 3.7 (e superior), uma comparação de instâncias de objetos para igualdade é um recurso embutido.

Um backport para Dataclasses está disponível 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
fonte
A apresentação PyCon 2018 de Raymond Hettinger é uma excelente maneira de começar com os Dataclasses do Python.
Sarath Chandra
1

Ao comparar instâncias de objetos, a __cmp__função é chamada.

Se o operador == não estiver funcionando para você por padrão, você sempre poderá redefinir a __cmp__função para o objeto.

Editar:

Como foi apontado, a __cmp__função está obsoleta desde o 3.0. Em vez disso, você deve usar os métodos de "comparação rica" .

Silfverstrom
fonte
1
A função cmp está obsoleta para 3.0+
Christopher
0

Eu escrevi isso e coloquei em um test/utilsmódulo no meu projeto. Nos casos em que não é uma classe, basta planejar o ditado, isso atravessará os dois objetos e garantirá

  1. todo atributo é igual à sua contraparte
  2. Não existem atributos pendentes (atributos que existem apenas em um objeto)

É grande ... não é sexy ... mas 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)

Você pode limpá-lo um pouco removendo o _asserte usando apenas o ol 'simples, assertmas a mensagem que você recebe quando falha é muito inútil.

rayepps
fonte
0

Você deve implementar o 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
fonte
2
Por favor edite sua resposta e adicionar mais explicações ao seu código, explicando por que é diferente dos outros dez respostas. Esta pergunta tem dez anos e já tem uma resposta aceita e várias de alta qualidade. Sem detalhes adicionais, sua resposta é de qualidade muito mais baixa em comparação com as outras e provavelmente será rebaixada ou excluída.
Das_Geek 12/11/19
0

Abaixo funciona (no meu teste limitado), fazendo uma comparação profunda entre duas hierarquias de objetos. Em lida com vários casos, incluindo os casos em que os próprios objetos ou seus atributos são dicionários.

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 é um código muito complicado, portanto, adicione os casos que possam não funcionar nos comentários.

Shital Shah
fonte
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
fonte
0

Se você está lidando com uma ou mais classes que não podem ser alteradas internamente, existem maneiras simples e genéricas de fazer isso que também não dependem de uma biblioteca específica do diff:

Método mais fácil e inseguro para objetos muito complexos

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

pickleé uma biblioteca de serialização muito comum para objetos Python e, portanto, poderá serializar praticamente qualquer coisa. No trecho acima, estou comparando o strde serializado acom o de b. Ao contrário do próximo método, este tem a vantagem de também verificar classes personalizadas.

O maior problema: devido a métodos específicos de ordenação e [de / en], picklepode não produzir o mesmo resultado para objetos iguais , especialmente ao lidar com objetos mais complexos (por exemplo, listas de instâncias de classes personalizadas aninhadas), como você frequentemente encontrará em algumas bibliotecas de terceiros. Para esses casos, eu recomendaria uma abordagem diferente:

Método completo e seguro para qualquer objeto

Você pode escrever uma reflexão recursiva que fornecerá objetos serializáveis ​​e comparar os 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:])

Agora, não importa quais são seus objetos, é garantida que a profunda igualdade 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

O número de comparáveis ​​também não importa

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

Meu caso de uso para isso foi verificar a profunda igualdade entre um conjunto diversificado de modelos de Machine Learning já treinados nos testes do BDD. Os modelos pertenciam a um conjunto diversificado de bibliotecas de terceiros. Certamente implementar __eq__como outras respostas aqui sugerem que não era uma opção para mim.

Cobrindo todas as bases

Você pode estar em um cenário em que uma ou mais das classes personalizadas que estão sendo comparadas não possuem uma __dict__implementação . Isso não é comum, por qualquer meio, mas é o caso de um subtipo dentro classificador aleatória Floresta do sklearn: <type 'sklearn.tree._tree.Tree'>. Trate essas situações caso a caso - por exemplo , especificamente , decidi substituir o conteúdo do tipo afetado pelo conteúdo de um método que me fornece informações representativas sobre a instância (nesse caso, o __getstate__método). Para isso, a penúltima linha da lista base_typedtornou-se

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

Edit: por uma questão de organização, substituí as duas últimas linhas base_typedpor return dict_from(obj)e implementei uma reflexão realmente genérica para acomodar bibliotecas mais obscuras (estou olhando para você, 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()}

Lembre-se de que nenhum dos métodos acima se aplica Truea objetos diferentes com os mesmos pares de chave-valor, mas com diferentes ordens de chave / valor, como em

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

Mas se você quiser, poderá usar o sortedmétodo interno do Python de qualquer maneira.

Julio Cezar Silva
fonte
-1

Se você deseja obter uma comparação atributo por atributo e ver se e onde ela falha, você pode usar a seguinte compreensão da lista:

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

A vantagem extra aqui é que você pode espremer uma linha e entrar na janela "Avaliar expressão" ao depurar no PyCharm.

DalyaG
fonte
-3

Eu tentei o exemplo inicial (veja 7 acima) e ele não funcionou no ipython. Observe que cmp (obj1, obj2) retorna um "1" quando implementado usando duas instâncias de objeto idênticas. Estranhamente, quando modifico um dos valores de atributo e recomparei, usando cmp (obj1, obj2) o objeto continua retornando um "1". (suspiro...)

Ok, então o que você precisa fazer é iterar dois objetos e comparar cada atributo usando o sinal ==.

user2215595
fonte
Pelo menos no Python 2.7, os objetos são comparados por identidade por padrão. Isso significa para o CPython em palavras práticas que eles comparam pelo endereço de memória. É por isso que cmp (O1, O2) Retorna 0 somente quando "o1 é o2" e consistentemente 1 ou -1, dependendo dos valores de id (o1) e ID (o2)
yacc143
-6

A instância de uma classe quando comparada com == é diferente de. A melhor maneira é atribuir a função cmp à sua classe, que fará as coisas.

Se você quiser fazer uma comparação pelo conteúdo, basta usar cmp (obj1, obj2)

No seu caso, cmp (doc1, doc2) retornará -1 se o conteúdo for o mesmo.

asb
fonte