__ne__是否应该作为__eq__的否定实现?

129 投票
4 回答
59480 浏览
提问于 2025-04-16 08:01

我有一个类,我想要重写 __eq__ 这个方法。重写这个方法是为了比较两个对象是否相等。看起来我也应该重写 __ne__ 这个方法,它是用来判断两个对象是否不相等的。那么,我应该把 __ne__ 实现成 __eq__ 的反面吗?这样做是否合适呢?

class A:

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

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

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

4 个回答

13

为了记录一下,一个标准的、可以在Python 2和Python 3之间通用的__ne__方法应该是这样的:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

这个方法可以和你可能定义的任何__eq__一起使用:

  • not (self == other)不同,这个方法在一些复杂的比较情况下不会干扰结果,比如当参与比较的类之一并不保证__ne__的结果和not__eq__上的结果相同的情况(例如在SQLAlchemy的ORM中,__eq____ne__都返回特殊的代理对象,而不是TrueFalse,试图对__eq__的结果使用not会返回False,而不是正确的代理对象)。
  • not self.__eq__(other)不同,当self.__eq__返回NotImplemented时,这个方法会正确地委托给另一个实例的__ne__not self.__eq__(other)会更错,因为NotImplemented是“真”的,所以当__eq__不知道如何进行比较时,__ne__会返回False,这意味着两个对象是相等的,而实际上询问的对象并不知道,这就意味着默认是不相等的)。

如果你的__eq__没有使用NotImplemented的返回值,这个方法是有效的(虽然有些多余),如果有时使用NotImplemented,这个方法可以正确处理。而且Python版本检查意味着如果这个类是在Python 3中被import的,__ne__会保持未定义,这样Python的本地高效的__ne__实现(上面提到的C版本)就可以接管。


为什么需要这个

Python的运算符重载规则

解释为什么要这样做而不是其他解决方案有点复杂。Python对运算符重载有一些通用规则,特别是比较运算符:

  1. (适用于所有运算符)当运行LHS OP RHS时,先尝试LHS.__op__(RHS),如果返回NotImplemented,再尝试RHS.__rop__(LHS)。例外:如果RHSLHS类的子类,那么先测试RHS.__rop__(LHS)。在比较运算符的情况下,__eq____ne__是它们自己的“反向运算符”(所以__ne__的测试顺序是LHS.__ne__(RHS),然后RHS.__ne__(LHS),如果RHSLHS的子类则顺序反转)。
  2. 除了“交换”运算符的概念外,运算符之间没有隐含关系。即使是同一个类的实例,LHS.__eq__(RHS)返回True也不意味着LHS.__ne__(RHS)返回False(实际上,这些运算符甚至不需要返回布尔值;像SQLAlchemy这样的ORM故意不这样做,以允许更具表现力的查询语法)。在Python 3中,默认的__ne__实现是这样工作的,但这并不是强制性的;你可以以不严格相反的方式重载__ne__

这如何应用于重载比较器

所以当你重载一个运算符时,你有两个任务:

  1. 如果你知道如何自己实现这个操作,就这样做,使用你自己对如何进行比较的知识(绝不要隐式或显式地委托给操作的另一方;这样做可能会导致不正确和/或无限递归,具体取决于你怎么做)。
  2. 如果你不知道如何自己实现这个操作,总是返回NotImplemented,这样Python就可以委托给另一个操作数的实现。

not self.__eq__(other)的问题

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

从不委托给另一方(如果__eq__正确返回NotImplemented,这就是错误的)。当self.__eq__(other)返回NotImplemented(这是“真”的),你默默地返回False,所以A() != something_A_knows_nothing_about返回False,而它应该检查something_A_knows_nothing_about是否知道如何与A的实例进行比较,如果不知道,它应该返回True(因为如果两边都不知道如何比较对方,它们被认为是不相等的)。如果A.__eq__实现不正确(在不识别另一方时返回False而不是NotImplemented),那么从A的角度来看这是“正确的”,返回True(因为A认为它不相等,所以它不相等),但从something_A_knows_nothing_about的角度来看可能是错误的,因为它甚至没有询问something_A_knows_nothing_aboutA() != something_A_knows_nothing_about最终返回True,但something_A_knows_nothing_about != A()可能返回False,或者其他任何返回值。

not self == other的问题

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

更微妙。对于99%的类来说,这个方法是正确的,包括所有__ne____eq__逻辑反转的类。但not self == other违反了上面提到的两个规则,这意味着对于__ne__ 不是__eq__逻辑反转的类,结果再次是不对称的,因为其中一个操作数从未被询问是否可以实现__ne__,即使另一个操作数不能。最简单的例子是一个奇怪的类,它对所有比较都返回False,所以A() == Incomparable()A() != Incomparable()都返回False。如果A.__ne__正确实现(在不知道如何进行比较时返回NotImplemented),那么关系是对称的;A() != Incomparable()Incomparable() != A()对结果达成一致(因为在前一种情况下,A.__ne__返回NotImplemented,然后Incomparable.__ne__返回False,而在后者中,Incomparable.__ne__直接返回False)。但是当A.__ne__被实现为return not self == other时,A() != Incomparable()返回True(因为A.__eq__返回,而不是NotImplemented,然后Incomparable.__eq__返回False,而A.__ne__将其反转为True),而Incomparable() != A()返回False

你可以在这里看到这个例子的实际效果。

显然,一个对__eq____ne__都总是返回False的类有点奇怪。但如前所述,__eq____ne__甚至不需要返回True/False;SQLAlchemy ORM中的类有比较器,返回一个特殊的代理对象用于查询构建,而不是True/False(在布尔上下文中它们是“真”的,但它们并不应该在这样的上下文中被评估)。

通过未能正确重载__ne__,你将会破坏这类对象,因为代码:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

将会正常工作(假设SQLAlchemy知道如何将MyClassWithBadNE插入到SQL字符串中;这可以通过类型适配器完成,而MyClassWithBadNE根本不需要配合),将预期的代理对象传递给filter,而:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

将最终传递给filter一个普通的False,因为self == other返回一个代理对象,而not self == other只是将这个“真”的代理对象转换为False。希望filter在处理像False这样的无效参数时会抛出异常。虽然我相信很多人会争辩说MyTable.fieldname 应该始终在比较的左侧,但事实是没有程序上的理由在一般情况下强制执行这一点,而一个正确的通用__ne__将无论如何都能工作,而return not self == other只在一种排列中有效。

183

在Python中,我应该根据__eq__来实现__ne__()运算符吗?

简短回答:不要实现它,但如果必须,使用==,而不是__eq__

在Python 3中,!=默认是==的反义词,所以你甚至不需要写__ne__,文档也不再强烈建议写一个。

一般来说,对于仅限Python 3的代码,除非你需要覆盖父类的实现,比如对于内置对象,否则不要写这个。

也就是说,记住Raymond Hettinger的评论

只有当__ne__在超类中没有定义时,__ne__方法才会自动跟随__eq__。所以,如果你是从内置类继承的,最好同时覆盖这两个。

如果你需要你的代码在Python 2中工作,按照Python 2的建议去做,它在Python 3中也能正常工作。

在Python 2中,Python本身不会自动根据其他操作来实现任何操作,因此你应该根据==来定义__ne__,而不是__eq__

class A(object):
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

请看下面的证明:

  • 根据__eq__实现__ne__()运算符,
  • 在Python 2中根本不实现__ne__

在下面的演示中会提供不正确的行为。

详细回答

Python 2的文档说:

比较运算符之间没有隐含关系。x==y为真并不意味着x!=y为假。因此,在定义__eq__()时,也应该定义__ne__(),以便运算符按预期工作。

这意味着如果我们根据__eq__的反义来定义__ne__,我们可以获得一致的行为。

文档的这一部分已经更新为Python 3:

默认情况下,__ne__()会委托给__eq__()并反转结果,除非返回NotImplemented

“新特性”部分中,我们看到这种行为发生了变化:

  • !=现在返回==的相反结果,除非==返回NotImplemented

对于实现__ne__,我们更倾向于使用==运算符,而不是直接使用__eq__方法,这样如果子类的self.__eq__(other)返回NotImplemented,Python会适当地检查other.__eq__(self)根据文档:

NotImplemented对象

这个类型只有一个值。只有一个对象具有这个值。这个对象通过内置名称NotImplemented访问。如果数值方法和丰富比较方法不实现提供的操作,它们可能返回这个值。(然后解释器会尝试反射操作,或者根据运算符使用其他后备方案。)它的真值为真。

当给定一个丰富的比较运算符时,如果它们不是同一类型,Python会检查other是否是子类型,并且如果它定义了该运算符,它会优先使用other的方法(对于<<=>=>是反向的)。如果返回NotImplemented那么它会使用对方的方法。(它不会两次检查同一个方法。)使用==运算符可以让这个逻辑得以实现。


期望

从语义上讲,你应该根据等于检查来实现__ne__,因为你的类的用户会期望以下函数对于所有A的实例是等价的:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

也就是说,上述两个函数应该始终返回相同的结果。但这取决于程序员。

当根据__eq__定义__ne__时,意外行为的演示:

首先是设置:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

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

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

实例化不等的实例:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

预期行为:

(注意:虽然下面每个断言的每秒都是等价的,因此在逻辑上对前一个是冗余的,但我包括它们是为了演示顺序在一个是另一个的子类时并不重要。

这些实例的__ne__是用==实现的:

assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1

这些实例在Python 3下测试也能正确工作:

assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1

请记住,这些是用__eq__实现的__ne__ - 虽然这是预期的行为,但实现是不正确的:

assert not wrong1 == wrong2         # These are contradicted by the
assert not wrong2 == wrong1         # below unexpected behavior!

意外行为:

注意,这个比较与上面的比较相矛盾(not wrong1 == wrong2)。

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

而且,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

在Python 2中不要跳过__ne__

为了证明你不应该在Python 2中跳过实现__ne__,请看这些等价对象:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

上述结果应该是False

Python 3源代码

默认的CPython实现__ne__typeobject.c中的object_richcompare

case Py_NE:
    /* By default, __ne__() delegates to __eq__() and inverts the result,
       unless the latter returns NotImplemented. */
    if (Py_TYPE(self)->tp_richcompare == NULL) {
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }
    res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
    if (res != NULL && res != Py_NotImplemented) {
        int ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        if (ok < 0)
            res = NULL;
        else {
            if (ok)
                res = Py_False;
            else
                res = Py_True;
            Py_INCREF(res);
        }
    }
    break;

但默认的__ne__使用__eq__

Python 3的默认__ne__实现细节在C层面上使用__eq__,因为更高层次的==PyObject_RichCompare)效率更低,因此它还必须处理NotImplemented

如果__eq__实现正确,那么==的反义也是正确的 - 这让我们可以避免在__ne__中处理低级实现细节。

使用==可以让我们将低级逻辑保持在一个地方,并避免__ne__中处理NotImplemented

人们可能错误地认为==可能返回NotImplemented

它实际上使用与__eq__的默认实现相同的逻辑,该逻辑检查身份(见do_richcompare和我们下面的证据)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

以及比较:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

性能

不要只听我说,让我们看看哪个更高效:

class CLevel:
    "Use default logic programmed in C"

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

class LowLevelPython:
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

我认为这些性能数字说明了一切:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

考虑到low_level_python在Python中执行的逻辑本来应该在C层面处理,这很有道理。

对一些批评者的回应

另一位回答者写道:

Aaron Hall的实现not self == other__ne__方法是不正确的,因为它永远不会返回NotImplementednot NotImplementedFalse),因此优先级较高的__ne__方法永远无法回退到优先级较低的__ne__方法。

__ne__永远不返回NotImplemented并不意味着它不正确。相反,我们通过与==的等式检查来处理NotImplemented的优先级。假设==实现正确,我们就完成了。

not self == other曾经是Python 3中__ne__方法的默认实现,但这是一个错误,并在2015年1月的Python 3.4中修正,正如ShadowRanger所注意到的(见问题#21408)。

好吧,让我们解释一下。

如前所述,Python 3默认处理__ne__时,首先检查self.__eq__(other)是否返回NotImplemented(一个单例) - 如果是,应该用is检查并返回,否则应该返回反义。以下是作为类混入的逻辑:

class CStyle__ne__:
    """Mixin that provides __ne__ functionality equivalent to 
    the builtin functionality
    """
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

这对于C级Python API的正确性是必要的,并且它是在Python 3中引入的,使得

变得多余。所有相关的__ne__方法都被移除,包括那些实现自己检查的,以及那些直接或通过==委托给__eq__的 - 而==是最常见的实现方式。

对称性重要吗?

我们的持续批评者提供了一个病态的例子,以支持在__ne__中处理NotImplemented,将对称性视为最重要的。让我们用一个清晰的例子来强化这个论点:

class B:
    """
    this class has no __eq__ implementation, but asserts 
    any instance is not equal to any other object
    """
    def __ne__(self, other):
        return True

class A:
    "This class asserts instances are equivalent to all other objects"
    def __eq__(self, other):
        return True

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)

所以,根据这个逻辑,为了保持对称性,我们需要编写复杂的__ne__,无论Python版本如何。

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return True
    def __ne__(self, other):
        result = other.__eq__(self)
        if result is NotImplemented:
            return NotImplemented
        return not result

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)

显然,我们不应该在意这些实例既相等又不相等。

我认为,对称性不如合理代码的假设和遵循文档建议重要。

然而,如果A有一个合理的__eq__实现,那么我们仍然可以遵循我的方向,这样我们仍然会有对称性:

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return False         # <- this boolean changed... 

>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)

结论

对于兼容Python 2的代码,使用==来实现__ne__。这更:

  • 正确
  • 简单
  • 高效

在仅限Python 3的情况下,使用C层面的低级反义 - 这甚至更简单和高效(尽管程序员负责确保它是正确的)。

再次强调,不要在高级Python中编写低级逻辑。

66

没问题,这样做完全可以。实际上,文档建议你在定义 __eq__ 的时候,也要定义 __ne__

比较操作符之间没有隐含的关系。也就是说,x==y 为真,并不意味着 x!=y 是假。因此,在定义 __eq__() 的时候,也应该定义 __ne__(),这样这些操作符的行为才会符合预期。

在很多情况下(比如这个例子),你只需要简单地把 __eq__ 的结果取反就可以了,但并不总是如此。

撰写回答