Порівняйте екземпляри об'єктів для рівності за їх атрибутами


244

У мене є клас MyClass, який містить дві змінні - члени fooі bar:

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

У мене є два екземпляри цього класу, кожен з яких має однакові значення для fooта bar:

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

Однак, коли я порівнюю їх за рівність, Python повертає False:

>>> x == y
False

Як я можу змусити пітон вважати ці два об’єкти рівними?

Відповіді:


354

Ви повинні реалізувати метод __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

Тепер він виводить:

>>> x == y
True

Зауважте, що реалізація __eq__автоматично зробить екземпляри вашого класу незмінними, це означає, що вони не можуть зберігатися в наборах і диктах. Якщо ви не моделюєте незмінний тип (тобто якщо атрибути fooі barможуть змінювати значення протягом життя вашого об’єкта), рекомендується просто залишити свої примірники непорушними.

Якщо ви моделюєте незмінний тип, вам слід також реалізувати гачок моделей даних __hash__:

class MyClass:
    ...

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

Загальне рішення, як ідея прокручування __dict__та порівняння значень, не є доцільним - воно ніколи не може бути по-справжньому загальним, оскільки в ньому __dict__можуть бути незрівнянні або нездатні типи.

Примітка: майте на увазі, що перед Python 3 вам може знадобитися використовувати __cmp__замість __eq__. Користувачі Python 2 також можуть захотіти реалізувати __ne__, оскільки розумне поведінка за замовчуванням для нерівності (тобто інвертування результату рівності) не буде створено автоматично в Python 2.


2
Мені було цікаво використовувати return NotImplemented(замість підвищення NotImplementedError). Ця тема покрита тут: stackoverflow.com/questions/878943 / ...
init_js

48

Ви переосмислюєте багаті оператори порівняння у вашому об’єкті.

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

Подобається це:

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

3
Зверніть увагу , що в Python 2.5 і далі, клас повинен визначити __eq__(), але тільки один з __lt__(), __le__(), __gt__()або __ge__()потрібно на додаток до цього. З цього Python може зробити висновок про інші методи. Див. Для functoolsотримання додаткової інформації.
kba

1
@kba, я не думаю, що це правда. Це може працювати для functoolsмодуля, але не працює для стандартних компараторів: MyObj1 != Myobj2буде працювати лише в тому випадку, якщо __ne__()метод реалізований.
Арел

6
Конкретною порадою щодо функціональних приладів має бути використання @functools.total_orderingдекоратора у вашому класі, тоді, як вище, ви можете визначити лише __eq__один і один, а решта буде виведено
Anentropic

7

Реалізуйте __eq__метод у своєму класі; щось на зразок цього:

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

Редагувати: якщо ви хочете, щоб ваші об'єкти порівнювали рівні, якщо і лише якщо вони мають рівні словники екземплярів:

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

Можливо, ви хочете сказати, self is otherчи є вони однаковим об'єктом.
S.Lott

2
-1. Навіть якщо це два екземпляри словника, Python автоматично порівнює їх за ключами / значеннями. Це не Java ...
e-satis

Перше рішення може підняти AttributeError. Ви повинні вставити рядок if hasattr(other, "path") and hasattr(other, "title"):(наприклад, цей приємний приклад у документації на Python).
Маджієро

5

Як підсумок:

  1. Рекомендується __eq__скоріше впроваджувати, а не __cmp__запускати python <= 2.0 ( __eq__додано в 2.1)
  2. Не забудьте також реалізувати __ne__(має бути щось на кшталт return not self.__eq__(other)або, return not self == otherза винятком дуже особливого випадку)
  3. Не забувайте, що оператор повинен бути впроваджений у кожному спеціальному класі, який ви хочете порівняти (див. Приклад нижче).
  4. Якщо ви хочете порівняти з об'єктом, який може бути None, ви повинні його реалізувати. Перекладач не може це здогадатися ... (див. Приклад нижче)

    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

Залежно від конкретного випадку, ви можете:

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

Дивіться словник Python з полів об’єкта


Також цікаво, хоча vars повертає дікт, але, здається, не працює, але навіть візуальний огляд показує, що вони насправді рівні. Я обійшов це, перетворивши дикти на рядки і порівнявши: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Ben

2

У Dataclasses в Python 3.7 (і вище) порівняння об'єктів для рівності є вбудованою особливістю.

Для 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

Презентація програми PyCon у 2018 році Raymond Hettinger - це відмінний спосіб розпочати роботу з класами даних Python.
Сарат Чандра

1

При порівнянні екземплярів об'єктів __cmp__функція викликається.

Якщо оператор == не працює для вас за замовчуванням, ви завжди можете переглянути __cmp__функцію для об'єкта.

Редагувати:

Як було зазначено, __cmp__функція застаріла з 3.0. Натомість слід скористатися методами «багатого порівняння» .


1
Функція cmp застаріла для версії 3.0+
Крістофер

1

Якщо ви маєте справу з одним або декількома класами, які ви не можете змінити зсередини, існують загальні та прості способи зробити це, які також не залежать від розробленої бібліотеки, що відрізняється:

Найпростіший, небезпечний для дуже складних об'єктів метод

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

pickleє дуже поширеною ланкою серіалізації для об'єктів Python, і таким чином зможе серіалізувати майже все, що насправді. У наведеному вище фрагменті я порівнюю strсеріалізований aз тим, від якого b. На відміну від наступного методу, цей має перевагу також у тому, щоб перевірити власні класи.

Найбільші клопоти: завдяки специфічним методам упорядкування та [de / en] методів кодування pickleможе не дати однакового результату для рівних об'єктів , особливо при роботі зі складнішими (наприклад, списками вкладених екземплярів спеціального класу), як ви часто зустрічаєте в деяких сторонніх губах. У цих випадках я рекомендую інший підхід:

Ретельний, безпечний для будь-якого об'єкта метод

Ви можете написати рекурсивне відображення, яке дасть вам серіалізаційні об'єкти, а потім порівняти результати

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

Тепер не має значення, які у вас об’єкти, глибока рівність гарантована

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

Кількість порівнянних також не має значення

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

Моїм прикладом для цього була перевірка глибокої рівності серед різноманітного набору вже навчених моделей машинного навчання всередині тестів BDD. Моделі належали до різноманітного набору сторонніх ліфтів. Безумовно, реалізація, __eq__як і інші відповіді тут, передбачає, що для мене це не було варіантом.

Покриває всі основи

Ви можете знаходитись у сценарії, коли один або кілька порівнюваних користувацьких класів не мають __dict__реалізації . Це не часто будь-якими засобами, але це той випадок підтипу в Random Forest класифікатором sklearn в: <type 'sklearn.tree._tree.Tree'>. Ставтесь до цих ситуацій у кожному конкретному випадку - наприклад, конкретно , я вирішив замінити вміст ураженого типу змістом методу, який дає мені репрезентативну інформацію про примірник (в даному випадку - __getstate__метод). Для таких, другий до останнього рядка в base_typedстали

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

Edit: заради організації, я замінив останні два рядки base_typedз return dict_from(obj), і реалізував дійсно родове відображення , щоб вмістити більше неясні LIBS (я дивлюся на вас, 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()}

Не майте на увазі, що жоден із перерахованих вище методів не поступається Trueдля різних об'єктів з однаковими парами ключ-значення, але різні замовлення ключ / значення, як у

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

Але якщо ви хочете, щоб ви могли sortedзаздалегідь скористатися вбудованим методом Python .


0

Я написав це і помістив його в test/utilsмодуль у своєму проекті. У випадках, коли його не є класом, просто плануйте ol 'dict, це дозволить перемістити обидва об'єкти та забезпечити

  1. кожен атрибут дорівнює його аналогу
  2. Немає атрибутів, що звисають (attrs, які існують лише на одному об'єкті)

Його велике ... його не сексуально ... але о бой це працює!

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)

Ви можете очистити його трохи, видаливши _assertта просто скориставшись простим ol ', assertале тоді повідомлення, яке ви отримуєте, коли воно не вдається, є дуже не корисним.


0

Ви повинні реалізувати метод __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
Будь ласка, відредагуйте свою відповідь та додайте додаткове пояснення до свого коду, пояснюючи, чому він відрізняється від десяти інших відповідей. На це питання десять років , і вже є прийнята відповідь та кілька дуже якісних. Без додаткових деталей, ваша відповідь набагато нижчої якості порівняно з іншими, і, швидше за все, буде залишена або видалена.
Das_Geek

0

Нижче працює (в моєму обмеженому тестуванні) шляхом глибокого порівняння двох ієрархій об'єктів. Обробляє різні випадки, включаючи випадки, коли самі об'єкти або їх атрибути є словниками.

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

Це дуже складний код, тому будь ласка, додайте будь-які випадки, які можуть не працювати для вас у коментарях.


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

Якщо ви хочете отримати порівняння атрибутів за атрибутами і побачити, якщо і де воно не вдається, ви можете використати таке розуміння списку:

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

Додаткова перевага тут полягає в тому, що ви можете видавити його на один рядок і ввести у вікно "Оцінити вираз" під час налагодження в PyCharm.


-3

Я спробував початковий приклад (див. 7 вище), і він не працював в ipython. Зауважте, що cmp (obj1, obj2) повертає "1" при реалізації, використовуючи два однакові екземпляри об'єкта. Як не дивно, коли я змінюю одне з значень атрибута і готую, використовуючи cmp (obj1, obj2), об'єкт продовжує повертати "1". (зітхає ...)

Гаразд, то, що вам потрібно зробити, це повторити два об’єкти та порівняти кожен атрибут за допомогою знака ==.


Принаймні, в Python 2.7 об'єкти порівнюються за ідентичністю за замовчуванням. Це означає, що CPython у практичних словах вони порівнюють за адресою пам'яті. Ось чому cmp (o1, o2) повертає 0 лише тоді, коли "o1 є o2" і послідовно 1 або -1 залежно від значень id (o1) та id (o2)
yacc143

-6

Екземпляр класу в порівнянні з == дорівнює нерівним. Найкращий спосіб - присвоїти функцію cmp своєму класу, який буде виконувати завдання.

Якщо ви хочете порівняти за вмістом, ви можете просто використовувати cmp (obj1, obj2)

У вашому випадку cmp (doc1, doc2) повернеться -1, якщо вміст мудрий, вони однакові.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.