Сравните экземпляры объектов на равенство по их атрибутам

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

Как я могу заставить python считать эти два объекта равными?

Желько Живкович
источник

Ответы:

355

Вы должны реализовать метод __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могут изменить значение в течение времени жизни вашего объекта), то рекомендуется просто оставить ваши экземпляры как не подлежащие изменению.

Если вы моделируете неизменный тип, вы также должны реализовать хук datamodel __hash__:

class MyClass:
    ...

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

Общее решение, такое как идея циклического прохождения __dict__и сравнения значений, не рекомендуется - оно никогда не может быть по-настоящему общим, поскольку в нем __dict__могут содержаться несопоставимые или непригодные типы.

NB: знайте, что до 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для получения дополнительной информации.
КБА
1
@kba, я не думаю, что это правда. Это может работать для functoolsмодуля, но не работает для стандартных компараторов: MyObj1 != Myobj2будет работать, только если __ne__()метод реализован.
Арель
6
конкретный совет о functools должен заключаться в использовании @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__
Kiv
источник
Возможно, вы self is otherхотите увидеть, являются ли они одним и тем же объектом.
S.Lott
2
-1. Даже если это два словарных экземпляра, Python автоматически сравнивает их по ключам / значениям. Это не Java ...
Э-Сат
Первое решение может поднять 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)
fievel
источник
2

В зависимости от вашего конкретного случая, вы можете сделать:

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

Смотрите словарь Python из полей объекта

user1338062
источник
Также интересно, хотя vars возвращает dict, assertDictEqual от unittest, похоже, не работает, хотя визуальный обзор показывает, что они фактически равны. Я справился с этим, превратив слова в строки и сравнив их: self.assertEqual (str (vars (tbl0)), str (vars (local_tbl0)))
Бен
2

В Dataclasses в Python 3.7 (и выше) сравнение экземпляров объектов на равенство является встроенной функцией.

Портировать для Dataclasses доступна для 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 году - отличный способ начать работу с Python Dataclasses.
Сарат Чандра
1

При сравнении экземпляров объектов __cmp__вызывается функция.

Если по умолчанию оператор == не работает для вас, вы всегда можете переопределить __cmp__функцию для объекта.

Редактировать:

Как уже было отмечено, эта __cmp__функция устарела с версии 3.0. Вместо этого вы должны использовать методы «богатого сравнения» .

Silfverstrom
источник
1
Функция cmp устарела для 3.0+
Кристофер
1

Если вы имеете дело с одним или несколькими классами, которые вы не можете изменить изнутри, есть универсальные и простые способы сделать это, которые также не зависят от библиотеки, специфичной для diff:

Самый простой, небезопасный метод для очень сложных объектов

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модуль в моем проекте. Для случаев, когда это не класс, просто спланируйте его, это обойдет оба объекта и обеспечит

  1. каждый атрибут равен своему аналогу
  2. Не существует висячих атрибутов (атрибуты, которые существуют только на одном объекте)

Его большой ... его не сексуально ... но о, бой, это работает!

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и просто используя обычный assertтекст, но тогда сообщение, которое вы получаете, когда оно терпит неудачу, очень бесполезно.

rayepps
источник
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
tomgtbst
источник
-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]

Дополнительным преимуществом здесь является то, что вы можете сжать его на одну строку и войти в окно «Evaluate Expression» при отладке в PyCharm.

DalyaG
источник
-3

Я попробовал начальный пример (см. 7 выше), и он не работал в ipython. Обратите внимание, что cmp (obj1, obj2) возвращает «1» при реализации с использованием двух идентичных экземпляров объекта. Как ни странно, когда я изменяю одно из значений атрибута и перекомпоновываю, используя cmp (obj1, obj2), объект продолжает возвращать «1». (вздох...)

Итак, вам нужно перебрать два объекта и сравнить каждый атрибут с помощью знака ==.

user2215595
источник
По крайней мере, в 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, если содержание будет одинаковым.

авь
источник