在Python类中支持等价(“相等”)的优雅方法

在编写自定义类时,通过==!=操作符允许等价通常是很重要的。在Python中,这可以通过分别实现__eq____ne__特殊方法实现。我发现最简单的方法是以下方法:

class Foo:
def __init__(self, item):
self.item = item


def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False


def __ne__(self, other):
return not self.__eq__(other)

你知道更优雅的方法吗?你知道使用上面比较__abc0的方法有什么特别的缺点吗?

请注意:一点澄清——当__eq____ne__未定义时,你会发现这样的行为:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

也就是说,a == b的计算结果为False,因为它实际上运行a is b,这是一个身份测试(即,“a是否与b是同一个对象?”)。

__eq____ne__被定义时,你会发现这样的行为(这是我们所追求的行为):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
289160 次浏览

你不必同时覆盖__eq____ne__,你可以只覆盖__cmp__,但这将对==,!==,<>等等。

is测试对象标识。这意味着当a和b都持有对同一对象的引用时,is b将为True。在python中,你总是在变量中保存一个对象的引用,而不是实际的对象,所以本质上,a是b,它们中的对象应该位于相同的内存位置。最重要的是,你要如何覆盖这种行为?

编辑:我不知道__cmp__已从python 3中删除,因此请避免使用它。

我认为你要找的两个术语是平等(==)和身份 (is)。例如:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object

你描述的方式就是我一直用的方式。由于它是完全泛型的,所以您总是可以将该功能分解到mixin类中,并在需要该功能的类中继承它。

class CommonEqualityMixin(object):


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


def __ne__(self, other):
return not self.__eq__(other)


class Foo(CommonEqualityMixin):


def __init__(self, item):
self.item = item

你需要小心继承:

>>> class Foo:
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False


>>> class Bar(Foo):pass


>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

更严格地检查类型,像这样:

def __eq__(self, other):
if type(other) is type(self):
return self.__dict__ == other.__dict__
return False

除此之外,你的方法也可以很好地工作,这就是特殊方法的用途。

'is'测试将使用内置的'id()'函数测试身份,该函数本质上返回对象的内存地址,因此不可重载。

然而,在测试类的相等性的情况下,你可能想对你的测试更严格一点,只比较你类中的数据属性:

import types


class ComparesNicely(object):


def __eq__(self, other):
for key, value in self.__dict__.iteritems():
if (isinstance(value, types.FunctionType) or
key.startswith("__")):
continue


if key not in other.__dict__:
return False


if other.__dict__[key] != value:
return False


return True

这段代码将只比较类的非函数数据成员,并跳过任何私有的东西,这通常是你想要的。在普通旧Python对象的情况下,我有一个实现__init__, __str__, __repr__和__eq__的基类,所以我的POPO对象不承担所有额外的(在大多数情况下相同)逻辑的负担。

这不是一个直接的回答,但似乎有足够的相关性,因为它有时省去了一些冗长乏味的内容。直接从医生那里剪下来的…


functools. total_orders (cls)

给定一个定义了一个或多个丰富比较排序方法的类,该类装饰器提供其余部分。这简化了指定所有可能的富比较操作所涉及的工作:

该类必须定义__lt__()__le__()__gt__()__ge__()中的一个。此外,该类应该提供一个__eq__()方法。

2.7新版功能

@total_ordering
class Student:
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))

考虑这个简单的问题:

class Number:


def __init__(self, number):
self.number = number




n1 = Number(1)
n2 = Number(1)


n1 == n2 # False -- oops

因此,Python默认使用对象标识符进行比较操作:

id(n1) # 140400634555856
id(n2) # 140400634555920

重写__eq__函数似乎可以解决这个问题:

def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return False




n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

Python 2中,始终记得重写__ne__函数,正如文档所述:

比较操作符之间没有隐含的关系。的 x==y为真并不意味着x!=y为假。因此,当 定义__eq__()时,还应该定义__ne__(),以便

def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)




n1 == n2 # True
n1 != n2 # False

Python 3中,这不再是必要的,正如文档所述:

默认情况下,__ne__()委托给__eq__()并反转结果 除非它是NotImplemented。没有其他暗示 比较运算符之间的关系,例如,真值 (x<y or x==y)(x<y or x==y)并不意味着x<=y

但这并不能解决我们所有的问题。让我们添加一个子类:

class SubNumber(Number):
pass




n3 = SubNumber(1)


n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

注意: Python 2有两类:

  • classic-style(或old-style)类,从object继承而不是,并声明为class A:class A():class A(B):,其中B是一个经典风格的类;

  • new-style类,继承自object并声明为class A(object)class A(B):,其中B是一个new-style类。Python 3只有被声明为class A:class A(object):class A(B):的新样式类。

对于经典风格的类,比较操作总是调用第一个操作数的方法,而对于新风格的类,它总是调用子类操作数与操作数的顺序无关的方法。

所以在这里,如果Number是一个经典风格的类:

  • n1 == n3调用n1.__eq__;
  • n3 == n1调用n3.__eq__;
  • n1 != n3调用n1.__ne__;
  • n3 != n1调用n3.__ne__

如果Number是一个新型类:

  • n1 == n3n3 == n1都调用n3.__eq__;
  • n1 != n3n3 != n1都调用n3.__ne__

为了修复Python 2经典风格类的==!=操作符的不可交换性问题,当不支持操作数类型时,__eq____ne__方法应该返回NotImplemented值。文档NotImplemented值定义为:

数值方法和丰富的比较方法可以返回此值,如果 它们不为所提供的操作数实现操作。( 解释器将尝试反射的操作或其他操作 后退,取决于操作人员。)它的真值为true

在这种情况下,操作符将比较操作委托给其他操作数的反映了方法文档将反射方法定义为:

这些方法没有参数互换的版本 当左参数不支持操作而支持右参数时 参数);相反,__lt__()__gt__()是彼此的 __le__()__ge__()是彼此的反射,并且 __eq__()__ne__()是它们自身的反射

结果如下所示:

def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented


def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is NotImplemented:
return NotImplemented
return not x

返回NotImplemented值而不是False是正确的做法,即使对于新样式的类,如果操作数类型不相关(没有继承),需要==!=操作符的交换性

我们到了吗?不完全是。我们有多少个唯一的数字?

len(set([n1, n2, n3])) # 3 -- oops

集合使用对象的哈希值,默认情况下Python返回对象标识符的哈希值。让我们试着重写它:

def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))


len(set([n1, n2, n3])) # 1

最终结果是这样的(我在最后添加了一些断言进行验证):

class Number:


def __init__(self, number):
self.number = number


def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented


def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is not NotImplemented:
return not x
return NotImplemented


def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))




class SubNumber(Number):
pass




n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)


assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1


assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1


assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1


assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2

从这个答案:https://stackoverflow.com/a/30676267/541136我已经证明,虽然它是正确的定义__ne__术语__eq__ -而不是

def __ne__(self, other):
return not self.__eq__(other)

你应该使用:

def __ne__(self, other):
return not self == other

我喜欢使用泛型类装饰器,而不是使用子类化/mixins

def comparable(cls):
""" Class decorator providing generic comparison functionality """


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


def __ne__(self, other):
return not self.__eq__(other)


cls.__eq__ = __eq__
cls.__ne__ = __ne__
return cls

用法:

@comparable
class Number(object):
def __init__(self, x):
self.x = x


a = Number(1)
b = Number(1)
assert a == b

这包含了对Algorias的答案的评论,并通过单个属性比较对象,因为我不关心整个字典。hasattr(other, "id")必须为真,但我知道它是真的,因为我在构造函数中设置了它。

def __eq__(self, other):
if other is self:
return True


if type(other) is not type(self):
# delegate to superclass
return NotImplemented


return other.id == self.id

我用__ne__的默认实现编写了一个自定义基,它简单地对__eq__求反:

class HasEq(object):
"""
Mixin that provides a default implementation of ``object.__neq__`` using the subclass's implementation of ``object.__eq__``.


This overcomes Python's deficiency of ``==`` and ``!=`` not being symmetric when overloading comparison operators
(i.e. ``not x == y`` *does not* imply that ``x != y``), so whenever you implement
`object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_, it is expected that you
also implement `object.__ne__ <https://docs.python.org/2/reference/datamodel.html#object.__ne__>`_


NOTE: in Python 3+ this is no longer necessary (see https://docs.python.org/3/reference/datamodel.html#object.__ne__)
"""


def __ne__(self, other):
"""
Default implementation of ``object.__ne__(self, other)``, delegating to ``self.__eq__(self, other)``.


When overriding ``object.__eq__`` in Python, one should also override ``object.__ne__`` to ensure that
``not x == y`` is the same as ``x != y``
(see `object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_ spec)


:return: ``NotImplemented`` if ``self.__eq__(other)`` returns ``NotImplemented``, otherwise ``not self.__eq__(other)``
"""
equal = self.__eq__(other)
# the above result could be either True, False, or NotImplemented
if equal is NotImplemented:
return NotImplemented
return not equal

如果你从这个基类继承,你只需要实现__eq__和基类。

回想起来,更好的方法可能是将其作为装饰器实现。类似@functools.total_ordering