Python,我是不是应该基于eq实现一个操作符?

2024-04-25 21:37:40 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个类,要在其中重写__eq__()运算符。我似乎也应该重写__ne__()运算符,但基于这样的__eq__实现__ne__有意义吗?

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

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

或者,Python使用这些操作符的方式有什么地方让我遗漏了,这不是一个好主意吗?


Tags: selfreturnvaluedef地方方式not运算符
3条回答

Python, should I implement __ne__() operator based on __eq__?

简而言之:不,用==代替__eq__

在Python 3中,!=在默认情况下是==的否定,因此您甚至不需要编写__ne__,文档也不再是对编写一个的固执己见。

一般来说,对于Python 3-only代码,除非需要覆盖父实现(例如,对于内置对象),否则不要编写。

也就是说,记住Raymond Hettinger's comment

The __ne__ method follows automatically from __eq__ only if __ne__ isn't already defined in a superclass. So, if you're inheriting from a builtin, it's best to override both.

如果您需要代码在Python 2中工作,请遵循python2的建议,它在Python 3中工作得很好。

在Python 2中,Python本身不会自动实现任何操作,因此,应该用==而不是__eq__来定义__ne__。 E、 G

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__
  • 根本不在Python 2中实现__ne__

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

冗长的回答

Python 2的documentation表示:

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.

所以这意味着,如果我们用__ne__的倒数定义__eq__,我们可以得到一致的行为。

此部分文档已更新为Python 3:

By default, __ne__() delegates to __eq__() and inverts the result unless it is NotImplemented.

"what's new" section中,我们看到这种行为已经改变了:

  • != now returns the opposite of ==, unless == returns NotImplemented.

对于实现__ne__,我们更喜欢使用==运算符而不是直接使用__eq__方法,这样,如果子类的self.__eq__(other)返回NotImplemented对于所检查的类型,Python将适当地检查other.__eq__(self)From the documentation

NotImplemented对象

This type has a single value. There is a single object with this value. This object is accessed through the built-in name NotImplemented. 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.

当给定一个丰富的比较运算符时,如果它们不是同一类型,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源代码

__ne__的默认CPython实现位于^{} in ^{}

    case Py_NE:
        /* By default, __ne__() delegates to __eq__() and inverts the result,
           unless the latter returns NotImplemented. */
        if (self->ob_type->tp_richcompare == NULL) {
            res = Py_NotImplemented;
            Py_INCREF(res);
            break;
        }
        res = (*self->ob_type->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);
            }
        }

我们看到了

但是默认的__ne__使用__eq__

Python 3在C级别的默认__ne__实现细节使用__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’s implementation not self == other of the __ne__ method is incorrect as it can never return NotImplemented (not NotImplemented is False) and therefore the __ne__ method that has priority can never fall back on the __ne__ method that does not have priority.

__ne__从不返回NotImplemented不会使其不正确。相反,我们通过检查与==是否相等来处理NotImplemented的优先级排序。假设==被正确实现,我们就完成了。

not self == other used to be the default Python 3 implementation of the __ne__ method but it was a bug and it was corrected in Python 3.4 on January 2015, as ShadowRanger noticed (see issue #21408).

好吧,我们来解释一下。

如前所述,Python 3默认处理__ne__,方法是首先检查self.__eq__(other)是否返回NotImplemented(singleton),如果返回is则应使用is进行检查,否则应返回相反值。下面是作为类mixin编写的逻辑:

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__的方法,而==是最常见的方法。

结论

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

  • 正确的
  • 简单的
  • 表演

仅在Python 3中,在C级别使用低级否定-它甚至更简单、更高效(尽管程序员负责确定它是正确的)。

同样,不要用高级Python编写低级逻辑。

是的,那很好。实际上,the documentation在定义__eq__时,会敦促您定义__ne__

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.

在很多情况下(比如这一个),它将像否定__eq__的结果一样简单,但并不总是如此。

仅作为记录,一个规范正确且跨Py2/Py3的可移植__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的结果相同(例如,SQLAlchemy的ORM,其中__eq____ne__都返回特殊的代理对象,而不是TrueFalse,并且尝试not__eq__的结果将返回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的本机高效回退^{} implementation (a C version of the above)接管。


为什么需要这个

Python重载规则

为什么你这样做,而不是其他解决方案的解释有些晦涩难懂。Python有几个关于重载运算符的一般规则,特别是比较运算符:

  1. (适用于所有运算符)运行LHS OP RHS时,请尝试LHS.__op__(RHS),如果返回NotImplemented,请尝试RHS.__rop__(LHS)。异常:如果RHSLHS类的一个子类,则首先测试RHS.__rop__(LHS)。在比较运算符的情况下,__eq____ne__是它们自己的“rop”(因此__ne__的测试顺序是LHS.__ne__(RHS),然后是RHS.__ne__(LHS),如果RHSLHS类的子类,则相反)
  2. 除了“交换”运算符的概念外,运算符之间没有隐含的关系。即使是同一个类,LHS.__eq__(RHS)返回True并不意味着LHS.__ne__(RHS)返回False(事实上,运算符甚至不需要返回布尔值;像SQLAlchemy这样的ORMs有意不返回,允许更具表现力的查询语法)。从Python 3开始,默认的__ne__实现以这种方式运行,但它不是契约式的;您可以以与__eq__完全相反的方式重写__ne__

如何应用于重载比较器

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

  1. 如果您自己知道如何实现该操作,那么就这样做,只使用您自己关于如何进行比较的知识(不要隐式或显式地委托给操作的另一端;这样做会有不正确和/或无限递归的风险,这取决于您是如何实现的)
  2. 如果您自己不知道如何实现该操作,总是返回NotImplemented,这样Python就可以委托给另一个操作数的实现

关于not self.__eq__(other)的问题

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

永远不要委托到另一边(如果__eq__正确返回NotImplemented则不正确)。当self.__eq__(other)返回NotImplemented(这是“truthy”)时,您无声地返回False,因此A() != something_A_knows_nothing_about返回False,当它应该检查something_A_knows_nothing_about是否知道如何与A的实例进行比较,如果不知道,它应该返回True(因为如果双方都不知道如何与对方进行比较,他们被认为不平等。如果A.__eq__的实现不正确(当它不识别另一面时返回False,而不是NotImplemented),那么从A的角度来看,这是“正确的”,返回True(因为A认为它不相等,所以它不等于l) ,但从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进行all比较,因此A() == Incomparable()A() != Incomparable()都返回False。如果A.__ne__(在不知道如何进行比较时返回NotImplemented)的正确实现,则关系是自反的;A() != Incomparable()Incomparable() != A()同意结果(因为在前一种情况下,A.__ne__返回NotImplemented,然后^}返回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.

您可以在操作here中看到这个示例。

显然,对于__eq____ne__总是返回False的类有点奇怪。但如前所述,__eq____ne__甚至不需要返回True/False;SQLAlchemy ORM有带比较器的类,这些比较器返回用于查询生成的特殊代理对象,而不是True/False(如果在布尔上下文中求值,则它们是“truthy”,但不应该在这样的上下文中求值)。

如果未能正确地重载__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只是将truthy代理对象转换为False。希望,filter在处理无效参数(如False)时引发异常。虽然我相信很多人会争辩说MyTable.fieldname应该在比较的左边保持一致,但事实上,在一般情况下,没有程序化的理由来强制执行这个,正确的泛型__ne__将以任何一种方式工作,而return not self == other只在一种安排中工作。

相关问题 更多 >