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


421

编写自定义类时,通过==!=运算符允许等效性通常很重要。在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)

您知道这样做更优雅的方法吗?您知道使用上述__dict__s 比较方法有什么特别的缺点吗?

注意:需要澄清一点-当__eq____ne__未定义时,您会发现以下行为:

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

也就是说,a == b评估为False因为它确实运行了a is b,所以对身份进行了测试(即“ ab?是同一对象”)。

__eq____ne__定义,你会发现这种行为(这是一个我们后):

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

6
+1,因为我不知道dict对于==使用成员式相等性,所以我假设只对相同的对象dict计算它们相等。我想这很明显,因为Python具有is运算符来区分对象标识和值比较。
SingleNegationElimination 2009年

5
我认为可以将接受的答案更正或重新分配给Algorias的答案,以便实施严格的类型检查。
最大

1
另外,还要确保哈希重写stackoverflow.com/questions/1608842/...
亚历Punnen

Answers:


328

考虑这个简单的问题:

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

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

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有两种类:

  • 经典样式(或旧样式)类,它们继承自object,并声明为class A:class A():或者经典样式类class A(B):在哪里B

  • 新样式类,那些从继承object和声明为class A(object)class A(B):其中B一个新式类。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__()and __gt__()是彼此的反射,__le__()and __ge__()是彼此的反射,and __eq__()and __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

如果操作数是不相关的类型(无继承),如果需要and 运算符的可交换性,那么即使对于新式类,也要返回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

3
hash(tuple(sorted(self.__dict__.items())))如果的值中有任何不可散列的对象self.__dict__(即,如果该对象的任何属性设置为a list),则将不起作用。
最高

3
没错,但是如果您的vars()中有这样的可变对象,那么这两个对象并不是真的相等...
Tal Weiss 2015年


1
三点评论:1.在Python 3中,不再需要实现__ne__:“默认情况下,除非结果为”,否则将__ne__()委托__eq__()并反转结果NotImplemented。2.如果仍然想实现__ne__,则可以使用一种更通用的实现(我认为Python 3使用的实现)x = self.__eq__(other); if x is NotImplemented: return x; else: return not x。3.给定__eq____ne__实现次优:if isinstance(other, type(self)):给出22 __eq__和10个__ne__调用,而if isinstance(self, type(other)):给出16 __eq__和6个__ne__调用。
Maggyero '11

4
他问起优雅,但他变得健壮。
GregNash19年

201

您需要小心继承:

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

除此之外,您的方法会很好地工作,这就是专用方法的目的。


这是个好的观点。我想值得注意的是,内置类型的子类仍然允许任意方向的相等,因此检查它是否是同一类型甚至是不可取的。
gotgenes

12
如果类型不同,我建议返回NotImplemented,将比较委托给rhs。
最长

4
@max比较不一定是从左侧(LHS)到右侧(RHS),然后是RHS与LHS。参见stackoverflow.com/a/12984987/38140。尽管如此,NotImplemented按照您的建议返回将始终导致superclass.__eq__(subclass),这是所需的行为。
gotgenes

4
如果您有大量的成员,并且周围没有很多对象副本,那么通常最好添加一个初始身份测试if other is self。这样避免了冗长的字典比较,并且在将对象用作字典键时可以节省大量资金。
丹妮·怀特

2
而且不要忘记实施__hash__()
Dane White

161

您描述的方式就是我一直以来所做的方式。由于它是完全通用的,因此您始终可以将该功能分解为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

6
+1:策略模式,可轻松替换子类。
S.Lott

3
isinstance很烂。为什么要检查?为什么不只是自我。__dict__ == other .__ dict__?
nosklo

3
@nosklo:我不明白..如果完全不相关的类中的两个对象碰巧具有相同的属性怎么办?
最大

1
我以为Nokslo建议跳过isinstance。在这种情况下,您将不再知道是否other是的子类self.__class__
最大

10
__dict__比较的另一个问题是,如果您不想在定义相等性时考虑某个属性(例如,唯一的对象ID或像创建时间戳的元数据),该怎么办。
亚当·帕金

14

这不是一个直接的答案,但似乎足够相关,可以解决,因为它有时可以节省一些冗长的乏味。从文档中直接切出...


functools.total_ordering(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()))

1
但是total_ordering有一个细微的陷阱:regebro.wordpress.com/2010/12/13/…。意识到 !
Mr_and_Mrs_D '16

8

您不必覆盖两者,__eq____ne__只能覆盖,__cmp__但这将对==,!==,<,>等结果产生影响。

is测试对象身份。这意味着,当a和b都持有对同一对象的引用时,isb就会出现True。在python中,您始终会在变量中持有对对象的引用,而不是实际对象,因此从本质上来说,如果a为b为true,则其中的对象应位于相同的内存位置。最重要的是,为什么您要继续压倒这种行为?

编辑:我不知道__cmp__从python 3中删除了,所以避免它。


因为有时您对对象的相等性有不同的定义。
Ed S.

is运算符为解释器提供了对象标识的答案,但您仍然可以通过覆盖cmp
Vasil

7
在Python 3中,“ cmp()函数消失了,不再支持__cmp __()特殊方法。” is.gd/aeGv
gotgenes


2

我认为您要查找的两个术语是相等(==)和同一性(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

1
也许,除了一个人可以创建一个只比较两个列表中前两个项目的类外,如果这些项目相等,则其结果为True。我认为这是等效,不是平等。仍然在eq中完全有效。
gotgenes

但是,我确实同意“是”是对身份的检验。
gotgenes

1

“ 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对象不承担所有额外(在大多数情况下相同)逻辑的负担。


有点挑剔,但仅当您尚未定义自己的is_()成员函数(2.3+)时,才使用id()测试'is'。[ docs.python.org/library/operator.html]
花费了

我认为“覆盖”实际上是指对操作员模块进行猴子修补。在这种情况下,您的陈述并不完全正确。提供操作员模块是为了方便起见,重写这些方法不会影响“ is”操作员的行为。使用“ is”进行的比较始终使用对象的id()进行比较,不能覆盖此行为。同样,is_成员函数对比较没有影响。
mcrute 2010年

mcrute-我讲得太早了(而且错误地讲了),您绝对正确。

这是一个非常好的解决方案,尤其是当__eq__将在中声明时CommonEqualityMixin(请参阅其他答案)。在比较从SQLAlchemy中的Base派生的类的实例时,我发现这特别有用。为了不进行比较,_sa_instance_state我更改key.startswith("__")):key.startswith("_")):。我也有一些反向引用,而来自Algorias的答案产生了无穷递归。因此,我将所有反向引用都命名为以开头,'_'以便在比较期间也将其跳过。注意:在Python 3.x中更改iteritems()items()
Wookie88

@mcrute通常,__dict__实例的开头没有任何内容,__除非由用户定义。之类的东西__class____init__等不在实例的__dict__,而是在它的类的__dict__。OTOH,私有属性可以轻松地以开头,__可能应该用于__eq__。您能否阐明跳过__前缀属性时您到底想避免什么?
最高

1

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

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

0

这合并了对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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.