【问题标题】:Compare object instances for equality by their attributes通过属性比较对象实例是否相等
【发布时间】:2010-11-16 15:52:05
【问题描述】:

我有一个类MyClass,它包含两个成员变量foobar

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

我有这个类的两个实例,每个实例都有相同的 foobar 值:

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

但是,当我比较它们是否相等时,Python 返回 False

>>> x == y
False

如何让 python 认为这两个对象相等?

【问题讨论】:

    标签: python equality


    【解决方案1】:

    比较对象的实例时,会调用__cmp__ 函数。

    如果默认情况下 == 运算符不适合您,您可以随时为对象重新定义 __cmp__ 函数。

    编辑:

    正如已经指出的,__cmp__ 函数自 3.0 起已弃用。 相反,您应该使用 “rich comparison” 方法。

    【讨论】:

    • cmp 函数在 3.0+ 中已弃用
    【解决方案2】:

    在你的类中实现__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 看看它们是否是同一个对象。
    • -1。即使这是两个字典实例,Python 也会自动按键/值比较它们。这不是 Java...
    • 第一个解决方案可以引发AttributeError。您必须插入行 if hasattr(other, "path") and hasattr(other, "title"):(如 Python 文档中的 this nice example)。
    【解决方案3】:

    您覆盖对象中的rich comparison operators

    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
    

    【讨论】:

    • 请注意,在 Python 2.5 及更高版本中,该类必须定义 __eq__(),但除此之外只需要 __lt__()__le__()__gt__()__ge__() 之一.由此,Python 可以推断出其他方法。请参阅functools 了解更多信息。
    • @kba,我认为这不是真的。这可能适用于 functools 模块,但不适用于标准比较器MyObj1 != Myobj2 仅在实现 __ne__() 方法时才有效。
    • 关于 functools 的具体提示应该是在你的类上使用 @functools.total_ordering 装饰器,然后如上所述你可以只定义 __eq__ 和另一个,其余的将被派生
    【解决方案4】:

    类的实例与 == 比较时不相等。最好的方法是将 cmp 函数分配给你的类,它会做这些事情。

    如果你想通过内容进行比较,你可以简单地使用 cmp(obj1,obj2)

    在你的情况下 cmp(doc1,doc2) 如果内容相同,它将返回 -1。

    【讨论】:

      【解决方案5】:

      你应该实现方法__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__ 将自动使您的类的实例不可散列,这意味着它们不能存储在集合和字典中。如果您不是在为不可变类型建模(即,如果属性 foobar 可能会在对象的生命周期内更改值),那么建议将您的实例保留为不可散列。

      如果你在为不可变类型建模,你还应该实现数据模型钩子__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 中不会自动创建不等式的合理默认行为(即反转等式结果)。

      【讨论】:

      • 我很好奇return NotImplemented 的使用(而不是提高NotImplementedError)。此处涵盖该主题:stackoverflow.com/questions/878943/…
      • 如果您使用的是python3.7或以上版本,请使用dataclasses
      • 只是关于NotImplemented 的注释:“3.9 版更改:不推荐在布尔上下文中评估 NotImplemented”(继续说在未来的版本中将发出 DeprecationWarning)。所以我想我们现在应该要么返回False,要么在绝对必要的情况下提出NotImplementedErrorFalse 让我觉得更可取,因为任何对象确实不等于另一个类的另一个: NotImplementedError 建议我们希望这是一种编码错误发现策略,但可能不是一个好的策略)。
      • 不可哈希对象不能存储在字典中是不正确的。他们能。例如。可以将列表存储在字典中。不能做的是用列表(或其他不可散列的对象)标记 dict 条目。
      【解决方案6】:

      我尝试了最初的示例(参见上面的 7),但它在 ipython 中不起作用。请注意,当使用两个相同的对象实例实现时, cmp(obj1,obj2) 返回“1”。奇怪的是,当我修改其中一个属性值并重新比较时,使用 cmp(obj1,obj2) 对象继续返回“1”。 (叹气……)

      好的,那么您需要做的是迭代两个对象并使用 == 符号比较每个属性。

      【讨论】:

      • 至少在 Python 2.7 中,默认情况下通过身份比较对象。这意味着对于 CPython 来说,他们通过内存地址进行比较。这就是为什么 cmp(o1, o2) 仅在“o1 为 o2”时返回 0,并且根据 id(o1) 和 id(o2) 的值始终为 1 或 -1
      【解决方案7】:

      总结一下:

      1. 建议实现__eq__ 而不是__cmp__,除非您运行python __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)
        

      【讨论】:

        【解决方案8】:

        如果您想逐个属性比较,并查看是否失败以及在哪里失败,您可以使用以下列表推导:

        [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 中调试时,可以挤一行,进入“Evaluate Expression”窗口。

        【讨论】:

          【解决方案9】:

          根据您的具体情况,您可以这样做:

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

          Python dictionary from an object's fields

          【讨论】:

          • 同样有趣的是,虽然 vars 返回一个 dict,但 unittest 的 assertDictEqual 似乎不起作用,尽管视觉审查表明它们实际上是相等的。我通过将字典转换为字符串并比较它们来解决这个问题: self.assertEqual(str(vars(tbl0)), str(vars(local_tbl0)))
          • 非常适合我的情况的解决方案,我无法更改课程本身。
          【解决方案10】:

          我写了这个并将它放在我项目的test/utils 模块中。对于不是类的情况,只需计划 ol' dict,这将遍历两个对象并确保

          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 来清理它,但是当它失败时您收到的消息非常无用。

          【讨论】:

            【解决方案11】:

            你应该实现方法__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
            

            【讨论】:

            • edit your answer 并为您的代码添加进一步解释,解释为什么它与其他十个答案不同。这个问题有十年的历史,并且已经有一个公认的答案和几个非常高质量的答案。如果没有其他详细信息,您的答案与其他答案相比质量要低得多,并且很可能会被否决或删除。
            【解决方案12】:

            下面的作品(在我有限的测试中)通过在两个对象层次结构之间进行深入比较。 In 处理各种情况,包括对象本身或其属性是字典的情况。

            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
            

            这是一个非常棘手的代码,因此请在 cmets 中添加任何可能不适合您的情况。

            【讨论】:

              【解决方案13】:
              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
              

              【讨论】:

                【解决方案14】:

                对于Dataclasses in Python 3.7(及以上),比较对象实例是否相等是一个内置功能。

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

                【讨论】:

                【解决方案15】:

                如果您正在处理一个或多个您无法从内部更改的类,则有一些通用且简单的方法可以做到这一点,它们也不依赖于特定于 diff图书馆:

                最简单但对非常复杂的对象不安全的方法

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

                pickle 是一个非常常见的 Python 对象序列化库,因此可以序列化几乎任何东西,真的。在上面的 sn-p 中,我将来自序列化的astr 与来自b 的一个进行比较。与下一种方法不同,这种方法的优点是还可以对自定义类进行类型检查。

                最大的麻烦:由于特定的排序和 [de/en] 编码方法,pickle may not yield the same result for equal objects,尤其是在处理更复杂的(例如嵌套自定义类实例的列表)时,就像您经常在第三个中发现的那样-派对库。对于这些情况,我建议采用不同的方法:

                彻底、对任何对象都安全的方法

                您可以编写一个递归反射,为您提供可序列化的对象,然后比较结果

                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__ 实现。这无论如何都不常见,但 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__()
                

                编辑:为了组织起见,我用return dict_from(obj) 替换了上面可怕的oneliner。在这里,dict_from 是一个非常通用的反射,用于容纳更多晦涩的库(我在看着你,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):
                        # Use standard dict representation when available
                        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
                

                但如果你愿意,无论如何你都可以事先使用 Python 的内置 sorted 方法。

                【讨论】:

                • 如果您将字典传递给 base_typed(),base_typed(obj) 中的行:base_items = [base_typed(item) for item in obj] 将只返回字典的键,字典中包含的所有实际数据都将丢失。
                【解决方案16】:

                使用setattr 函数。当您无法在类本身中添加某些内容时(例如,在导入类时),您可能想要使用它。

                setattr(MyClass, "__eq__", lambda x, y: x.foo == y.foo and x.bar == y.bar)
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-01-31
                  • 2016-03-05
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多