Porównaj instancje obiektów pod kątem równości według ich atrybutów


244

Mam klasę MyClass, która zawiera dwie zmienne składowe fooi bar:

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

Mam dwa wystąpienia tej klasy, z których każda ma identyczne wartości dla fooi bar:

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

Jednak gdy porównuję je pod kątem równości, Python zwraca False:

>>> x == y
False

Jak sprawić, aby python uznał te dwa obiekty za równe?

Odpowiedzi:


354

Powinieneś zaimplementować metodę __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

Teraz wyświetla:

>>> x == y
True

Zauważ, że implementacja __eq__automatycznie sprawi, że instancje twojej klasy nie będą dostępne, co oznacza, że ​​nie mogą być przechowywane w zestawach i nagraniach. Jeśli nie modelujesz niezmiennego typu (tj. Jeśli atrybuty fooi barmogą zmienić wartość w trakcie życia obiektu), zalecamy pozostawienie instancji jako nie do powstrzymania.

Jeśli modelujesz niezmienny typ, powinieneś również zaimplementować hook modelu danych __hash__:

class MyClass:
    ...

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

Ogólne rozwiązanie, takie jak pomysł zapętlania __dict__i porównywania wartości, nie jest wskazane - nigdy nie może być naprawdę ogólne, ponieważ __dict__mogą zawierać nieporównywalne lub niemożliwe do skasowania typy.

Uwaga: pamiętaj, że przed Python 3 może być konieczne użycie __cmp__zamiast __eq__. Użytkownicy Pythona 2 mogą również chcieć zaimplementować __ne__, ponieważ rozsądne zachowanie domyślne nierówności (tj. Odwrócenie wyniku równości) nie zostanie automatycznie utworzone w Pythonie 2.


2
Byłem ciekawy użycia return NotImplemented(zamiast podbijania NotImplementedError). Temat ten jest omawiany tutaj: stackoverflow.com/questions/878943/…
init_js

48

Zastępujesz operatory porównania bogatego w swoim obiekcie.

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

Lubię to:

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

3
Zauważ, że w Pythonie 2.5 i dalej, klasa musi zdefiniować __eq__(), ale tylko jeden __lt__(), __le__(), __gt__()czy __ge__()potrzebna jest oprócz tego. Na tej podstawie Python może wywnioskować inne metody. Zobacz functoolspo więcej informacji.
kba

1
@kba, nie sądzę, że to prawda. Może to działać w przypadku functoolsmodułu, ale nie działa w przypadku standardowych komparatorów: MyObj1 != Myobj2działa tylko, jeśli __ne__()metoda jest zaimplementowana.
Arel

6
konkretną wskazówką dotyczącą funkuntów powinno być użycie @functools.total_orderingdekoratora w swojej klasie, a następnie, jak wyżej, możesz zdefiniować tylko __eq__jedno i drugie, a reszta zostanie wyprowadzona
Anentropic

7

Zaimplementuj __eq__metodę w swojej klasie; coś takiego:

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

Edycja: jeśli chcesz, aby Twoje obiekty były równe wtedy i tylko wtedy, gdy mają słowniki równych wystąpień:

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

Być może chcesz self is othersprawdzić, czy są one tym samym przedmiotem.
S.Lott,

2
-1. Nawet jeśli jest to dwa wystąpienie słownikowe, Python automatycznie porówna je według kluczy / wartości. To nie jest Java ...
e-satis

Pierwsze rozwiązanie może podnieść AttributeError. Musisz wstawić linię if hasattr(other, "path") and hasattr(other, "title"):(jak ten ładny przykład w dokumentacji Pythona).
Maggyero

5

Podsumowując:

  1. Zaleca się __eq__raczej implementację niż __cmp__, chyba że uruchomisz Python <= 2.0 ( __eq__został dodany w 2.1)
  2. Nie zapomnij również wdrożyć __ne__(powinno to być coś wyjątkowego return not self.__eq__(other)lub return not self == otherwyjątek)
  3. Nie zapominaj, że operator musi być zaimplementowany w każdej klasie niestandardowej, którą chcesz porównać (patrz przykład poniżej).
  4. Jeśli chcesz porównać z obiektem, który może być Brak, musisz go zaimplementować. Tłumacz nie może zgadnąć ... (patrz przykład poniżej)

    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

W zależności od konkretnego przypadku możesz:

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

Zobacz słownik Pythona z pól obiektu


Interesujące jest również to, że chociaż vars zwraca dyktandę, assertDictEqual firmy unittest nie wydaje się działać, mimo że przegląd wizualny pokazuje, że w rzeczywistości są one równe. Obejrzałem to, przekształcając dyktanda w ciągi i porównując je: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben

2

W przypadku Dataclasses w Pythonie 3.7 (i nowszych) porównanie instancji obiektów dla równości jest wbudowaną funkcją.

Backportu do Dataclasses dostępne Pythona 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

Prezentacja PyCon 2018 Raymonda Hettingera to doskonały sposób na rozpoczęcie korzystania z Python Dataclasses.
Sarath Chandra

1

Podczas porównywania instancji obiektów __cmp__wywoływana jest funkcja.

Jeśli operator == nie działa dla Ciebie domyślnie, zawsze możesz ponownie zdefiniować __cmp__funkcję dla obiektu.

Edytować:

Jak już wspomniano, __cmp__funkcja jest przestarzała od 3.0. Zamiast tego powinieneś użyć metod „bogatego porównania” .


1
Funkcja cmp jest przestarzała w wersji 3.0+
Christopher

1

Jeśli masz do czynienia z jedną lub kilkoma klasami, których nie możesz zmienić od wewnątrz, istnieją ogólne i proste sposoby, aby to zrobić, które również nie zależą od biblioteki specyficznej dla różnic:

Najłatwiejsza, najbardziej niebezpieczna metoda dla bardzo złożonych obiektów

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

picklejest bardzo popularną biblioteką serializacji dla obiektów Pythona i dlatego będzie w stanie serializować praktycznie wszystko. W powyższym fragmencie porównuję numer strseryjny az kodem z b. W przeciwieństwie do kolejnej metody, ta ma tę zaletę, że sprawdza także niestandardowe klasy.

Największy problem: ze względu na specyficzne metody porządkowania i kodowania [de / en] picklemogą nie dawać tego samego wyniku dla równych obiektów , szczególnie w przypadku bardziej złożonych (np. List zagnieżdżonych instancji klasy niestandardowej), jak to często można znaleźć w niektórych bibliotekach stron trzecich. W takich przypadkach zaleciłbym inne podejście:

Dokładna, bezpieczna dla dowolnego obiektu metoda

Możesz napisać rekurencyjną refleksję, która da ci obiekty do serializacji, a następnie porównać wyniki

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

Teraz nie ma znaczenia, jakie są twoje obiekty, zapewniona jest głęboka równość

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

Liczba porównywalnych elementów również nie ma znaczenia

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

Moim przypadkiem użycia było sprawdzenie głębokiej równości między różnorodnym zestawem już przeszkolonych modeli uczenia maszynowego w testach BDD. Modele należały do ​​różnorodnego zestawu bibliotek stron trzecich. Z pewnością zastosowanie __eq__innych odpowiedzi tutaj sugeruje, że nie było dla mnie opcją.

Obejmuje wszystkie bazy

Możesz znajdować się w scenariuszu, w którym co najmniej jedna z porównywanych klas niestandardowych nie ma __dict__implementacji . To nie jest powszechne w jakikolwiek sposób, ale jest to przypadek podtypu w sklearn Losowa Lasu klasyfikatora: <type 'sklearn.tree._tree.Tree'>. Traktować te sytuacje, w każdym indywidualnym przypadku - np konkretnie , postanowiłem zastąpić zawartość strapionych typu z treścią metody, która daje mi informacje na reprezentatywnej instancji (w tym przypadku __getstate__metoda). W tym celu base_typedstał się drugi do ostatniego rzędu

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

Edit: dla dobra organizacji, wymieniłem dwa ostatnie wiersze base_typedz return dict_from(obj), i wdrożył naprawdę rodzajowy refleksji aby pomieścić więcej niejasnych bibliotekami (Patrzę na ciebie, 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()}

Nieważne, że żadna z powyższych metod nie daje wyników Truedla różnych obiektów o tych samych parach klucz-wartość, ale o różnych porządkach klucz / wartość, jak w

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

Ale jeśli chcesz, możesz wcześniej skorzystać z wbudowanej sortedmetody Pythona .


0

Napisałem to i umieściłem w test/utilsmodule w moim projekcie. W przypadkach, gdy nie jest to klasa, wystarczy po prostu zaplanować, przejdzie to przez oba obiekty i zapewni

  1. każdy atrybut jest równy swojemu odpowiednikowi
  2. Nie ma żadnych wiszących atrybutów (atrty, które istnieją tylko na jednym obiekcie)

Jest duży ... nie jest seksowny ... ale och boi to działa!

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)

Możesz to trochę posprzątać, usuwając _asserti używając zwykłego ol ', assertale wtedy wiadomość, którą otrzymasz, gdy zawiedzie, jest bardzo nieprzydatna.


0

Powinieneś zaimplementować metodę __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
Proszę edytować swoje odpowiedzi i dodać dalsze wyjaśnienia do kodu, wyjaśniając dlaczego jest ona różna od dziesięciu innych odpowiedzi. To pytanie ma dziesięć lat i ma już zaakceptowaną odpowiedź oraz kilka bardzo wysokiej jakości. Bez dodatkowych szczegółów Twoja odpowiedź jest znacznie niższej jakości w porównaniu do innych i najprawdopodobniej zostanie oceniona lub usunięta.
Das_Geek

0

Poniżej działa (w moich ograniczonych testach), dokonując głębokiego porównania między dwiema hierarchiami obiektów. Obsługuje różne przypadki, w tym przypadki, gdy same obiekty lub ich atrybuty są słownikami.

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

Jest to bardzo trudny kod, więc w komentarzach dodaj wszelkie przypadki, które mogą nie działać.


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

Jeśli chcesz uzyskać porównanie atrybut po atrybucie i sprawdzić, czy i gdzie się nie powiedzie, możesz skorzystać z następującego opisu listy:

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

Dodatkową zaletą jest to, że możesz wycisnąć go z jednej linii i wejść w okno „Oceń wyrażenie” podczas debugowania w PyCharm.


-3

Próbowałem początkowego przykładu (patrz 7 powyżej) i nie działał w ipython. Zauważ, że cmp (obj1, obj2) zwraca „1”, gdy jest implementowany przy użyciu dwóch identycznych instancji obiektów. Co dziwne, kiedy modyfikuję jedną z wartości atrybutów i dokonuję ponownej porównania, używając cmp (obj1, obj2) obiekt nadal zwraca „1”. (westchnienie...)

Ok, więc musisz powtórzyć dwa obiekty i porównać każdy atrybut za pomocą znaku ==.


Przynajmniej w Pythonie 2.7 obiekty są domyślnie porównywane według tożsamości. Oznacza to, że dla CPython w praktycznych słowach porównują według adresu pamięci. Dlatego cmp (o1, o2) zwraca 0 tylko wtedy, gdy „o1 to o2” i konsekwentnie 1 lub -1 w zależności od wartości id (o1) i id (o2)
yacc143

-6

Wystąpienie klasy w porównaniu z == nie jest równe. Najlepszym sposobem jest przypisanie funkcji cmp do klasy, która wykona te czynności.

Jeśli chcesz dokonać porównania według zawartości, możesz po prostu użyć cmp (obj1, obj2)

W twoim przypadku cmp (doc1, doc2) Zwróci -1, jeśli zawartość jest taka sama.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.