比较对象实例的属性是否相等

我有一个类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认为这两个对象相等?

311941 次浏览

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

如果==操作符默认不适用,则始终可以为该对象重新定义__cmp__函数。

编辑:

正如已经指出的,__cmp__函数自3.0以来已弃用。 相反,你应该使用“富比较”方法

在你的类中实现__eq__方法;就像这样:

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

编辑:如果你想让你的对象比较相等当且仅当它们有相等的实例字典:

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

重写对象中的丰富的比较运算符

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

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

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

在你的例子中,cmp(doc1,doc2)如果它们的内容相同,它将返回-1。

你应该实现方法__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中不会自动创建一个合理的不等式默认行为(即颠倒相等结果)。

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

好的,所以你需要做的是迭代两个对象,并使用==号比较每个属性。

总结如下:

  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)
    

如果你想逐个属性进行比较,并查看它是否以及在哪里失败,你可以使用下面的列表推导式:

[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”窗口中输入。

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

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

看到Python字典从对象的字段

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

  1. 每个属性都与其对应的属性相等
  2. 不存在悬空属性(只存在于一个对象上的attrs)

它的大…它不是性感的…但是,哦,boi,它工作!

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,但当它失败时,你得到的消息是非常没有帮助的。

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

下面通过在两个对象层次结构之间进行深度比较来工作(在我有限的测试中)。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

这是一个非常棘手的代码,所以请在注释中添加任何可能不适合你的情况。

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

对于Python 3.7中的数据类(及以上),对象实例的相等性比较是一个内置特性。

数据类的backport可用于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

如果你正在处理不能从内部更改的一个或多个类,有一些通用和简单的方法来做到这一点,这些方法也不依赖于特定于diff的库:

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

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

pickle是Python对象的一个非常常见的序列化库,因此能够序列化几乎任何东西。在上面的代码片段中,我比较了来自序列化的astr和来自bstr。与下一个方法不同,这个方法还具有类型检查自定义类的优点。

最大的麻烦:由于特定的排序和[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__实现。这无论如何都不常见,但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)替换了上面丑陋的一行程序。这里,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方法。

使用setattr函数。当您不能在类本身中添加内容时,例如,当您导入类时,您可能想要使用此方法。

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