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

时间:2008-12-23 22:30:53

标签: python equality equivalence

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

11 个答案:

答案 0 :(得分:256)

考虑这个简单的问题:

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__函数,因为documentation状态:

  

比较运营商之间没有隐含的关系。该   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 中,不再需要这样做,因为documentation表示:

  

默认情况下,__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有两种类:

  • classic-style (或旧式)类,继承自object并且声明为class A:class A():class A(B):,其中B是经典风格的类;

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

对于经典样式类,比较操作总是调用第一个操作数的方法,而对于新样式类,它总是调用子类操作数regardless of the order of the operands的方法。

所以在这里,如果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__

要修复{2}经典样式类的==!=运算符的非交换性问题,__eq____ne__方法应返回{{ 1}}不支持操作数类型时的值。 documentationNotImplemented值定义为:

  

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

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

  

这些方法没有交换参数版本(可以使用   当左参数不支持操作但右边   论证确实);相反,NotImplemented__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 not NotImplemented: return not x return NotImplemented 和{{1}的交换,即使对于新式课程,返回NotImplemented值而不是False也是正确的做法当操作数是不相关的类型(没有继承)时,需要运算符。

我们在吗?不完全的。我们有多少个唯一号码?

==

设置使用对象的哈希值,默认情况下,Python返回对象标识符的哈希值。我们试着覆盖它:

!=

最终结果如下所示(我在最后添加了一些断言用于验证):

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

答案 1 :(得分:191)

你需要小心继承:

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

除此之外,您的方法将正常工作,这就是特殊方法。

答案 2 :(得分:155)

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

答案 3 :(得分:13)

不是一个直接的答案,但似乎有足够的相关性,因为它有时会节省一些冗长的单调乏味。直接从文档中删除...


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

答案 4 :(得分:8)

您不必覆盖只能覆盖__eq__的{​​{1}}和__ne__,但这会对==,!==,&lt;的结果产生影响。 ,&gt;等等。

__cmp__测试对象标识。这意味着当a和b都持有对同一对象的引用时,is b将为is。在python中,你始终对变量中的对象而不是实际对象进行引用,因此对于a来说,a b是真的,它们中的对象应该位于同一个内存位置。你最重要的是为什么要重写这种行为?

编辑:我不知道从python 3中删除了True所以请避免使用它。

答案 5 :(得分:3)

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

答案 6 :(得分:2)

我认为您正在寻找的两个术语是相等(==)和身份(是)。例如:

>>> 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 :(得分: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

此代码仅比较您班级的非功能数据成员以及跳过任何私人通常所需的内容。在Plain Old Python Objects的情况下,我有一个基类,它实现__init __,__ ttr __,_ _ rpr__和__eq__,因此我的POPO对象不承担所有额外(在大多数情况下是相同的)逻辑的负担。

答案 8 :(得分:0)

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

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

答案 9 :(得分: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

答案 10 :(得分:0)

我用 __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