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

问题:在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)

您知道这样做更优雅的方法吗?您知道使用上述__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

When writing custom classes it is often important to allow equivalence by means of the == and != operators. In Python, this is made possible by implementing the __eq__ and __ne__ special methods, respectively. The easiest way I’ve found to do this is the following method:

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)

Do you know of more elegant means of doing this? Do you know of any particular disadvantages to using the above method of comparing __dict__s?

Note: A bit of clarification–when __eq__ and __ne__ are undefined, you’ll find this behavior:

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

That is, a == b evaluates to False because it really runs a is b, a test of identity (i.e., “Is a the same object as b?”).

When __eq__ and __ne__ are defined, you’ll find this behavior (which is the one we’re after):

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

回答 0

考虑这个简单的问题:

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

Consider this simple problem:

class Number:

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


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

n1 == n2 # False -- oops

So, Python by default uses the object identifiers for comparison operations:

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

Overriding the __eq__ function seems to solve the problem:

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

In Python 2, always remember to override the __ne__ function as well, as the documentation states:

There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false. Accordingly, when defining __eq__(), one should also define __ne__() so that the operators will behave as expected.

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


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

In Python 3, this is no longer necessary, as the documentation states:

By default, __ne__() delegates to __eq__() and inverts the result unless it is NotImplemented. There are no other implied relationships among the comparison operators, for example, the truth of (x<y or x==y) does not imply x<=y.

But that does not solve all our problems. Let’s add a subclass:

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

Note: Python 2 has two kinds of classes:

  • classic-style (or old-style) classes, that do not inherit from object and that are declared as class A:, class A(): or class A(B): where B is a classic-style class;

  • new-style classes, that do inherit from object and that are declared as class A(object) or class A(B): where B is a new-style class. Python 3 has only new-style classes that are declared as class A:, class A(object): or class A(B):.

For classic-style classes, a comparison operation always calls the method of the first operand, while for new-style classes, it always calls the method of the subclass operand, regardless of the order of the operands.

So here, if Number is a classic-style class:

  • n1 == n3 calls n1.__eq__;
  • n3 == n1 calls n3.__eq__;
  • n1 != n3 calls n1.__ne__;
  • n3 != n1 calls n3.__ne__.

And if Number is a new-style class:

  • both n1 == n3 and n3 == n1 call n3.__eq__;
  • both n1 != n3 and n3 != n1 call n3.__ne__.

To fix the non-commutativity issue of the == and != operators for Python 2 classic-style classes, the __eq__ and __ne__ methods should return the NotImplemented value when an operand type is not supported. The documentation defines the NotImplemented value as:

Numeric methods and rich comparison methods may return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true.

In this case the operator delegates the comparison operation to the reflected method of the other operand. The documentation defines reflected methods as:

There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.

The result looks like this:

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

Returning the NotImplemented value instead of False is the right thing to do even for new-style classes if commutativity of the == and != operators is desired when the operands are of unrelated types (no inheritance).

Are we there yet? Not quite. How many unique numbers do we have?

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

Sets use the hashes of objects, and by default Python returns the hash of the identifier of the object. Let’s try to override it:

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

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

The end result looks like this (I added some assertions at the end for validation):

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

回答 1

您需要小心继承:

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

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

You need to be careful with inheritance:

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

Check types more strictly, like this:

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

Besides that, your approach will work fine, that’s what special methods are there for.


回答 2

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

The way you describe is the way I’ve always done it. Since it’s totally generic, you can always break that functionality out into a mixin class and inherit it in classes where you want that functionality.

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

回答 3

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


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

Not a direct answer but seemed relevant enough to be tacked on as it saves a bit of verbose tedium on occasion. Cut straight from the docs…


functools.total_ordering(cls)

Given a class defining one or more rich comparison ordering methods, this class decorator supplies the rest. This simplifies the effort involved in specifying all of the possible rich comparison operations:

The class must define one of __lt__(), __le__(), __gt__(), or __ge__(). In addition, the class should supply an __eq__() method.

New in version 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()))

回答 4

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

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

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

You don’t have to override both __eq__ and __ne__ you can override only __cmp__ but this will make an implication on the result of ==, !==, < , > and so on.

is tests for object identity. This means a is b will be True in the case when a and b both hold the reference to the same object. In python you always hold a reference to an object in a variable not the actual object, so essentially for a is b to be true the objects in them should be located in the same memory location. How and most importantly why would you go about overriding this behaviour?

Edit: I didn’t know __cmp__ was removed from python 3 so avoid it.


回答 5

从这个答案: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

From this answer: https://stackoverflow.com/a/30676267/541136 I have demonstrated that, while it’s correct to define __ne__ in terms __eq__ – instead of

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

you should use:

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

回答 6

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

I think that the two terms you’re looking for are equality (==) and identity (is). For example:

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

回答 7

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

The ‘is’ test will test for identity using the builtin ‘id()’ function which essentially returns the memory address of the object and therefore isn’t overloadable.

However in the case of testing the equality of a class you probably want to be a little bit more strict about your tests and only compare the data attributes in your class:

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

This code will only compare non function data members of your class as well as skipping anything private which is generally what you want. In the case of Plain Old Python Objects I have a base class which implements __init__, __str__, __repr__ and __eq__ so my POPO objects don’t carry the burden of all that extra (and in most cases identical) logic.


回答 8

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

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

Instead of using subclassing/mixins, I like to use a generic class decorator

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

Usage:

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

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

回答 9

这合并了对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

This incorporates the comments on Algorias’ answer, and compares objects by a single attribute because I don’t care about the whole dict. hasattr(other, "id") must be true, but I know it is because I set it in the constructor.

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