__ne__ 应该作为 __eq__ 的否定来实现吗?
我有一个类,我想重写 __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)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
简短回答:不要实现它,但如果必须,请使用
==
,而不是__eq__
在 Python 3 中,
!=
是否定默认情况下为==
,因此您甚至不需要编写__ne__
,并且文档不再坚持编写一个。一般来说,对于仅 Python 3 的代码,不要编写代码,除非您需要掩盖父实现(例如,对于内置对象)。
也就是说,请记住Raymond Hettinger 的评论:
如果您需要代码在 Python 2 中工作,请遵循 Python 2 的建议,它将在 Python 3 中正常工作。
在 Python 2 中,Python 本身不会自动实现任何根据另一个操作的操作 - 因此,您应该根据
==
定义__ne__
而不是__eq__
。EG
请参阅以下证明,证明
__eq__
实现__ne__()
运算符,并且__ne__
会提供不正确的行为。
长答案
Python 2 的 文档 说:
因此,这意味着如果我们根据
__eq__
的逆定义__ne__
,我们可以获得一致的行为。文档的这一部分已针对 Python 3: 进行了更新
在“新增内容”部分中,我们看到这种行为已经改变:
为了实现
__ne__
,我们更喜欢使用==
运算符而不是直接使用__eq__
方法,这样如果子类的self.__eq__(other)
对于检查的类型返回NotImplemented
,Python 将适当地检查other.__eq__(self)
来自文档:当给定丰富的比较运算符时,如果它们不是同一类型,Python 会检查 other 是否是子类型,如果定义了该运算符,则使用 other > 的方法优先(与
<
、<=
、>=
和>
相反) 。如果返回NotImplemented
,则它使用相反的方法。 (它不会检查同一个方法两次。)使用==
运算符可以实现此逻辑。期望
从语义上讲,您应该根据相等性检查来实现 __ne__ ,因为您的类的用户将期望以下函数对于 A 的所有实例都是等效的:
也就是说,上述两个函数都应该总是返回相同的结果。但这取决于程序员。
基于
__eq__
定义__ne__
时意外行为的演示:首先设置:
实例化非等效实例:
预期行为:(
注意:当以下每项的每一个断言与之前的实例是等价的,因此在逻辑上是多余的,我将它们包括在内是为了证明当一个实例是另一个实例的子类时,顺序并不重要。)
这些实例具有
__ne__==
实现的 code>:这些实例在 Python 3 下测试,也可以正常工作:
并且回想一下,这些实例已经使用
__eq__
实现了__ne__
-虽然这是预期的行为,但实现是不正确的:意外的行为:
请注意,此比较与上面的比较相矛盾(
不是错误 1 == 错误 2
)。并且,
不要在 Python 2 中跳过
__ne__
有关您不应在 Python 2 中跳过实现
__ne__
的证据,请参阅这些等效对象:上面的结果应该是
错误!
Python 3 源
__ne__
的默认 CPython 实现位于object_richcompare
中的typeobject.c
:但是默认的
__ne__
使用__eq__
?Python 3 在 C 级别的默认
__ne__
实现细节使用__eq__
因为更高级别的==
(PyObject_RichCompare) 效率较低 - 因此它还必须处理NotImplemented
。如果
__eq__
正确实现,那么==
的否定也是正确的 - 它允许我们避免__ne__
中的低级实现细节。使用
==
允许我们将低级逻辑保留在一个位置,并避免在中处理
。NotImplemented
>__ne__人们可能会错误地认为
==
可能返回NotImplemented
。它实际上使用与
__eq__
的默认实现相同的逻辑,该实现检查身份(请参阅do_richcompare 以及下面的证据)以及比较:
性能
不要相信我的话,让我们看看什么性能更好:
我认为这些性能数字不言而喻:
当您考虑到
low_level_python
正在 Python 中执行原本在 C 级别处理的逻辑时,这是有道理的。对一些批评的回应
另一位回答者写道:
让
__ne__
永远不会返回NotImplemented
并不意味着它不正确。相反,我们通过检查==
的相等性来处理NotImplemented
的优先级。假设==
已正确实现,我们就完成了。好吧,让我们解释一下。
如前所述,Python 3 默认情况下通过首先检查
self.__eq__(other)
是否返回NotImplemented
(单例)来处理__ne__
- 这应该使用is
进行检查,如果是则返回,否则应返回相反值。这是作为类 mixin 编写的逻辑:这对于 C 级 Python API 的正确性是必要的,它是在 Python 3 中引入的,使得
__ne__
方法 用于关闭 问题 21408 和__ne__
方法后续清理删除了此处的多余内容。所有相关的
__ne__
方法都被删除,包括实现自己检查的方法以及直接或通过==
委托给__eq__
的方法 - 和 < code>== 是最常见的方法。对称性重要吗?
我们坚持不懈的批评家提供了一个病态的例子来证明在
__ne__
中处理NotImplemented
的情况,重视对称性高于一切。让我们用一个清晰的例子来论证这个论点:所以,按照这个逻辑,为了保持对称性,我们需要编写复杂的
__ne__
,无论Python版本如何。显然,我们不应该介意这些实例既相等又不相等。
我认为对称性不如合理代码的假设和遵循文档的建议重要。
但是,如果 A 有一个
__eq__ 的合理实现,那么我们仍然可以遵循我的方向,并且仍然具有对称性:
结论
对于 Python 2 兼容代码,使用
==
实现__ne__
。更重要的是:仅在 Python 3 中,在 C 级别上使用低级否定 - 它甚至更简单且性能更高(尽管程序员有责任确定它是 >正确)。
再次强调,不要用高级 Python 编写低级逻辑。
Short Answer: Don't implement it, but if you must, use
==
, not__eq__
In Python 3,
!=
is the negation of==
by default, so you are not even required to write a__ne__
, and the documentation is no longer opinionated on writing one.Generally speaking, for Python 3-only code, don't write one unless you need to overshadow the parent implementation, e.g. for a builtin object.
That is, keep in mind Raymond Hettinger's comment:
If you need your code to work in Python 2, follow the recommendation for Python 2 and it will work in Python 3 just fine.
In Python 2, Python itself does not automatically implement any operation in terms of another - therefore, you should define the
__ne__
in terms of==
instead of the__eq__
.E.G.
See proof that
__ne__()
operator based on__eq__
and__ne__
in Python 2 at allprovides incorrect behavior in the demonstration below.
Long Answer
The documentation for Python 2 says:
So that means that if we define
__ne__
in terms of the inverse of__eq__
, we can get consistent behavior.This section of the documentation has been updated for Python 3:
and in the "what's new" section, we see this behavior has changed:
For implementing
__ne__
, we prefer to use the==
operator instead of using the__eq__
method directly so that ifself.__eq__(other)
of a subclass returnsNotImplemented
for the type checked, Python will appropriately checkother.__eq__(self)
From the documentation:When given a rich comparison operator, if they're not the same type, Python checks if the
other
is a subtype, and if it has that operator defined, it uses theother
's method first (inverse for<
,<=
,>=
and>
). IfNotImplemented
is returned, then it uses the opposite's method. (It does not check for the same method twice.) Using the==
operator allows for this logic to take place.Expectations
Semantically, you should implement
__ne__
in terms of the check for equality because users of your class will expect the following functions to be equivalent for all instances of A.:That is, both of the above functions should always return the same result. But this is dependent on the programmer.
Demonstration of unexpected behavior when defining
__ne__
based on__eq__
:First the setup:
Instantiate non-equivalent instances:
Expected Behavior:
(Note: while every second assertion of each of the below is equivalent and therefore logically redundant to the one before it, I'm including them to demonstrate that order does not matter when one is a subclass of the other.)
These instances have
__ne__
implemented with==
:These instances, testing under Python 3, also work correctly:
And recall that these have
__ne__
implemented with__eq__
- while this is the expected behavior, the implementation is incorrect:Unexpected Behavior:
Note that this comparison contradicts the comparisons above (
not wrong1 == wrong2
).and,
Don't skip
__ne__
in Python 2For evidence that you should not skip implementing
__ne__
in Python 2, see these equivalent objects:The above result should be
False
!Python 3 source
The default CPython implementation for
__ne__
is intypeobject.c
inobject_richcompare
:But the default
__ne__
uses__eq__
?Python 3's default
__ne__
implementation detail at the C level uses__eq__
because the higher level==
(PyObject_RichCompare) would be less efficient - and therefore it must also handleNotImplemented
.If
__eq__
is correctly implemented, then the negation of==
is also correct - and it allows us to avoid low level implementation details in our__ne__
.Using
==
allows us to keep our low level logic in one place, and avoid addressingNotImplemented
in__ne__
.One might incorrectly assume that
==
may returnNotImplemented
.It actually uses the same logic as the default implementation of
__eq__
, which checks for identity (see do_richcompare and our evidence below)And the comparisons:
Performance
Don't take my word for it, let's see what's more performant:
I think these performance numbers speak for themselves:
This makes sense when you consider that
low_level_python
is doing logic in Python that would otherwise be handled on the C level.Response to some critics
Another answerer writes:
Having
__ne__
never returnNotImplemented
does not make it incorrect. Instead, we handle prioritization withNotImplemented
via the check for equality with==
. Assuming==
is correctly implemented, we're done.Well, let's explain this.
As noted earlier, Python 3 by default handles
__ne__
by first checking ifself.__eq__(other)
returnsNotImplemented
(a singleton) - which should be checked for withis
and returned if so, else it should return the inverse. Here is that logic written as a class mixin:This is necessary for correctness for C level Python API, and it was introduced in Python 3, making
__ne__
methods in this patch to close Issue 21408 and__ne__
methods in the follow-on cleanup removed hereredundant. All relevant
__ne__
methods were removed, including ones implementing their own check as well as ones that delegate to__eq__
directly or via==
- and==
was the most common way of doing so.Is Symmetry Important?
Our persistent critic provides a pathological example to make the case for handling
NotImplemented
in__ne__
, valuing symmetry above all else. Let's steel-man the argument with a clear example:So, by this logic, in order to maintain symmetry, we need to write the complicated
__ne__
, regardless of Python version.Apparently we should give no mind that these instances are both equal and not equal.
I propose that symmetry is less important than the presumption of sensible code and following the advice of the documentation.
However, if A had a sensible implementation of
__eq__
, then we could still follow my direction here and we would still have symmetry:Conclusion
For Python 2 compatible code, use
==
to implement__ne__
. It is more:In Python 3 only, use the low-level negation on the C level - it is even more simple and performant (though the programmer is responsible for determining that it is correct).
Again, do not write low-level logic in high level Python.
是的,那很好。事实上,文档敦促您定义
__ne__ 当您定义
__eq__
时:在很多情况下(例如本例),它就像否定 __eq__ 的结果一样简单,但并非总是如此。
Yes, that's perfectly fine. In fact, the documentation urges you to define
__ne__
when you define__eq__
:In a lot of cases (such as this one), it will be as simple as negating the result of
__eq__
, but not always.仅供记录,规范正确且跨 Py2/Py3 可移植的
__ne__
看起来像:这适用于您可能定义的任何
__eq__
:not (self = = other)
,不会干扰一些涉及比较的恼人/复杂的情况,其中涉及的类之一并不意味着 __ne__ 的结果与 的结果相同not
on__eq__
(例如 SQLAlchemy 的 ORM,其中__eq__
和__ne__
返回特殊代理对象,而不是True
或False
,并且尝试不
__eq__
的结果将返回False
,而不是正确的代理对象)。not self.__eq__(other)
不同,当self.__eq__
返回NotImplemented< 时,这会正确委托给另一个实例的
__ne__
/code> (not self.__eq__(other)
会是额外的错误,因为NotImplemented
是真的,所以当__eq__
不知道如何时要执行比较,__ne__
将返回False
,这意味着两个对象相等,而实际上唯一询问的对象并不知道,这意味着默认值不相等)如果您的
__eq__
不使用NotImplemented
返回,则此方法有效(具有无意义的开销),如果它有时确实使用NotImplemented
,则此方法可以处理它适当地。 Python 版本检查意味着,如果该类在 Python 3 中被import
-ed,则__ne__
未定义,从而允许 Python 原生、高效的回退__ne__
实现(上述的 C 版本)接管。为什么需要这样做
Python 重载规则
为什么这样做而不是其他解决方案的解释有点神秘。 Python 对于重载运算符(尤其是比较运算符)有一些一般规则:(
LHS OP RHS
时,尝试LHS.__op__(RHS)
,如果返回NotImplemented
,请尝试RHS.__rop__(LHS)
。例外:如果RHS
是LHS
类的子类,则先测试RHS.__rop__(LHS)
首先 。对于比较运算符,__eq__
和__ne__
是它们自己的“rop”(因此__ne__
的测试顺序是LHS .__ne__(RHS)
,然后是RHS.__ne__(LHS)
,如果RHS
是LHS
类的子类,则相反)LHS.__eq__(RHS)
返回True
并不意味着LHS.__ne__(RHS)
返回False(事实上,运算符甚至不需要返回布尔值;像 SQLAlchemy 这样的 ORM 故意不这样做,从而允许更具表现力的查询语法)。从 Python 3 开始,默认的 __ne__ 实现就以这种方式运行,但它不是契约性的;您可以以与
__eq__
不严格相反的方式覆盖__ne__
。这如何适用于重载比较器
因此,当您重载运算符时,您有两项工作:
NotImplemented
,因此Python可以委托给其他操作数的实现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
认为它不相等,所以它不相等),但它可能是从something_A_knows_nothing_about
的角度来看,这是错误的,因为它甚至从未询问过something_A_knows_nothing_about
;A() != Something_A_knows_nothing_about
最终为True
,但something_A_knows_nothing_about != A()
可能False
,或任何其他返回值。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__
code> 返回NotImplemented
,然后Incomparable.__ne__
返回False
,而后者返回Incomparable.__ne__
<直接代码>False)。但是,当A.__ne__
实现为return not self == other
时,A() != Incomparable()
返回True< /code> (因为
将其反转为A.__eq__
返回,而不是NotImplemented
,然后Incomparable.__eq__
返回False
,并且 < code>A.__ne__True
),而Incomparable() != A()
返回False。
您可以查看实际操作示例 此处。
显然,对于
__eq__
和__ne__
总是返回False
的类有点奇怪。但如前所述,__eq__
和__ne__
甚至不需要返回True
/False
; SQLAlchemy ORM 具有带有比较器的类,该比较器返回用于查询构建的特殊代理对象,而不是True
/False
(如果在布尔上下文中评估,它们是“true” ,但它们永远不应该在这样的上下文中进行评估)。如果无法正确重载 __ne__ ,您将破坏此类类,因为代码:
将起作用(假设 SQLAlchemy 知道如何插入 MyClassWithBadNE )完全转换为 SQL 字符串;这可以使用类型适配器来完成,而无需
MyClassWithBadNE
完全配合),将预期的代理对象传递给filter
,而:传递
filter
一个普通的False
,因为self == other
返回一个代理对象,并且不是 self == other
只是将 true 代理对象转换为 False。希望filter
在处理诸如False
之类的无效参数时抛出异常。虽然我确信很多人会认为MyTable.fieldname
应该 始终位于比较的左侧,但事实仍然是,没有任何编程理由来强制执行此操作在一般情况下,正确的通用__ne__
可以以任何一种方式工作,而return not self == other
只能在一种安排中工作。Just for the record, a canonically correct and cross Py2/Py3 portable
__ne__
would look like:This works with any
__eq__
you might define:not (self == other)
, doesn't interfere with in some annoying/complex cases involving comparisons where one of the classes involved doesn't imply that the result of__ne__
is the same as the result ofnot
on__eq__
(e.g. SQLAlchemy's ORM, where both__eq__
and__ne__
return special proxy objects, notTrue
orFalse
, and trying tonot
the result of__eq__
would returnFalse
, rather than the correct proxy object).not self.__eq__(other)
, this correctly delegates to the__ne__
of the other instance whenself.__eq__
returnsNotImplemented
(not self.__eq__(other)
would be extra wrong, becauseNotImplemented
is truthy, so when__eq__
didn't know how to perform the comparison,__ne__
would returnFalse
, implying that the two objects were equal when in fact the only object asked had no idea, which would imply a default of not equal)If your
__eq__
doesn't useNotImplemented
returns, this works (with meaningless overhead), if it does useNotImplemented
sometimes, this handles it properly. And the Python version check means that if the class isimport
-ed in Python 3,__ne__
is left undefined, allowing Python's native, efficient fallback__ne__
implementation (a C version of the above) to take over.Why this is needed
Python overloading rules
The explanation of why you do this instead of other solutions is somewhat arcane. Python has a couple general rules about overloading operators, and comparison operators in particular:
LHS OP RHS
, tryLHS.__op__(RHS)
, and if that returnsNotImplemented
, tryRHS.__rop__(LHS)
. Exception: IfRHS
is a subclass ofLHS
's class, then testRHS.__rop__(LHS)
first. In the case of comparison operators,__eq__
and__ne__
are their own "rop"s (so the test order for__ne__
isLHS.__ne__(RHS)
, thenRHS.__ne__(LHS)
, reversed ifRHS
is a subclass ofLHS
's class)LHS.__eq__(RHS)
returningTrue
does not implyLHS.__ne__(RHS)
returnsFalse
(in fact, the operators aren't even required to return boolean values; ORMs like SQLAlchemy intentionally do not, allowing for a more expressive query syntax). As of Python 3, the default__ne__
implementation behaves this way, but it's not contractual; you can override__ne__
in ways that aren't strict opposites of__eq__
.How this applies to overloading comparators
So when you overload an operator, you have two jobs:
NotImplemented
, so Python can delegate to the other operand's implementationThe problem with
not self.__eq__(other)
never delegates to the other side (and is incorrect if
__eq__
properly returnsNotImplemented
). Whenself.__eq__(other)
returnsNotImplemented
(which is "truthy"), you silently returnFalse
, soA() != something_A_knows_nothing_about
returnsFalse
, when it should have checked ifsomething_A_knows_nothing_about
knew how to compare to instances ofA
, and if it doesn't, it should have returnedTrue
(since if neither side knows how to compare to the other, they're considered not equal to one another). IfA.__eq__
is incorrectly implemented (returningFalse
instead ofNotImplemented
when it doesn't recognize the other side), then this is "correct" fromA
's perspective, returningTrue
(sinceA
doesn't think it's equal, so it's not equal), but it might be wrong fromsomething_A_knows_nothing_about
's perspective, since it never even askedsomething_A_knows_nothing_about
;A() != something_A_knows_nothing_about
ends upTrue
, butsomething_A_knows_nothing_about != A()
couldFalse
, or any other return value.The problem with
not self == other
is more subtle. It's going to be correct for 99% of classes, including all classes for which
__ne__
is the logical inverse of__eq__
. Butnot self == other
breaks both of the rules mentioned above, which means for classes where__ne__
isn't the logical inverse of__eq__
, the results are once again non-symmetric, because one of the operands is never asked if it can implement__ne__
at all, even if the other operand can't. The simplest example is a weirdo class which returnsFalse
for all comparisons, soA() == Incomparable()
andA() != Incomparable()
both returnFalse
. With a correct implementation ofA.__ne__
(one which returnsNotImplemented
when it doesn't know how to do the comparison), the relationship is symmetric;A() != Incomparable()
andIncomparable() != A()
agree on the outcome (because in the former case,A.__ne__
returnsNotImplemented
, thenIncomparable.__ne__
returnsFalse
, while in the latter,Incomparable.__ne__
returnsFalse
directly). But whenA.__ne__
is implemented asreturn not self == other
,A() != Incomparable()
returnsTrue
(becauseA.__eq__
returns, notNotImplemented
, thenIncomparable.__eq__
returnsFalse
, andA.__ne__
inverts that toTrue
), whileIncomparable() != A()
returnsFalse.
You can see an example of this in action here.
Obviously, a class that always returns
False
for both__eq__
and__ne__
is a little strange. But as mentioned before,__eq__
and__ne__
don't even need to returnTrue
/False
; the SQLAlchemy ORM has classes with comparators that returns a special proxy object for query building, notTrue
/False
at all (they're "truthy" if evaluated in a boolean context, but they're never supposed to be evaluated in such a context).By failing to overload
__ne__
properly, you will break classes of that sort, as the code:will work (assuming SQLAlchemy knows how to insert
MyClassWithBadNE
into a SQL string at all; this can be done with type adapters withoutMyClassWithBadNE
having to cooperate at all), passing the expected proxy object tofilter
, while:will end up passing
filter
a plainFalse
, becauseself == other
returns a proxy object, andnot self == other
just converts the truthy proxy object toFalse
. Hopefully,filter
throws an exception on being handled invalid arguments likeFalse
. While I'm sure many will argue thatMyTable.fieldname
should be consistently on the left hand side of the comparison, the fact remains that there is no programmatic reason to enforce this in the general case, and a correct generic__ne__
will work either way, whilereturn not self == other
only works in one arrangement.__ne__
的正确实现@ShadowRanger 对特殊方法
__ne__
的实现是正确的:它也恰好是特殊方法
__ne__
的默认实现> 自 Python 3.4 起,如 Python 文档:另请注意,为不支持的操作数返回值
NotImplemented
并不特定于特殊方法__ne__
。事实上,所有特殊比较方法1和特殊数值方法2都应该针对不支持的操作数返回值NotImplemented
,如 Python 文档中指定:Python 中给出了特殊数字方法的示例文档:
1特殊比较方法:
__lt__
、__le__
、__eq__
、__ne__
、__gt__
和__ge__
。2 特殊数值方法:
__add__
、__sub__
、__mul__
、__matmul__
、<代码>__truediv__、__floordiv__
、__mod__
、__divmod__
、__pow__
、__lshift__
code>、__rshift__
、__and__
、__xor__
、__or__
及其__r*__
反射和 __i*__ 就地对应。__ne__
的错误实现 #1@Falmarri 对特殊方法
__ne__
的实现不正确:此实现的问题在于它没有依赖于特殊方法
另一个操作数的 __ne__
,因为它永远不会返回值NotImplemented
(表达式not self.__eq__(other)
计算结果为值True 或
False
,包括当其子表达式self.__eq__(other)
计算结果为值NotImplemented
时,因为表达式bool(NotImplemented )
计算结果为值True
)。值NotImplemented
的布尔计算破坏了比较运算符!=
和==
之间的补码关系:不正确
__ne__
的实现 #2@AaronHall 对特殊方法
__ne__
的实现也是不正确的:这个实现的问题在于它直接依赖于特殊方法
__eq__
另一个操作数,绕过另一个操作数的特殊方法__ne__
,因为它永远不会返回值NotImplemented
(表达式not self == other
依靠另一个操作数的特殊方法__eq__
并计算出值True
或False
)。绕过方法是不正确的,因为该方法可能有副作用 就像更新对象的状态:理解比较运算
在数学中,二元关系 集合X上的R是一组有序对(x,y >) X2。 R 中的语句 (x,y) 内容为“x 是 R” -与y'相关,并用xRy表示。
集合X上的二元关系R的属性:
例如,=。然而 ≠ 只是对称的。
例如,≤和≥。
例如,<和>。然而 ≠ 只是非自反的。
集合X上的两个二元关系R和S的运算:
始终有效的比较关系之间的关系:
仅对connex顺序关系有效的比较关系之间的关系:
因此,要在 Python 中正确实现比较运算符
==
、!=
、<
、>
、< code><=、>=
分别对应比较关系=、≠、<、>、≤、≥,以上数学性质和关系均应成立。比较操作
x运算符y
调用其操作数之一的类的特殊比较方法__operator__
:因为R是自反< /em> 暗示 xRx,自反比较操作
x 运算符 y
(x == y
,x <= y
code> 和x >= y
) 或自反特殊比较方法调用x.__operator__(y)
(x.__eq__(y)
,如果x
,则x.__le__(y)
和x.__ge__(y)
) 的计算结果应为值True
和y
相同,即如果表达式x is y
的计算结果为True
。由于R是无反意味着不是xRx,所以无反比较操作x运算符y
(x != y
、x < y
和x > y
) 或非自反特殊比较方法调用x.__operator__(y)
(x.__ne__(y)
、x.__lt__(y)
和x.__gt__(y)
) 应计算为值如果
>。 Python 认为比较运算符x
和y
相同,即表达式x is y
计算结果为True
,则为 False==
和关联的特殊比较方法__eq__
具有自反属性,但 令人惊讶的是没有考虑比较运算符<=< /code> 和
和关联的特殊比较方法>=
以及相关的特殊比较方法__le__
和__ge__
,并且 Python 认为比较运算符 < 具有非自反属性code>!=__ne__
但 令人惊讶的是没有考虑比较运算符<
和>
以及关联的特殊比较方法__lt__
和__gt__
。被忽略的比较运算符会引发异常TypeError
(并且关联的特殊比较方法会返回值NotImplemented
),如 Python 文档:类
object
提供了特殊比较方法的默认实现,这些方法由其所有子类继承,如 Python 文档:由于R = (RT)T,比较xRy相当于相反比较yRTx(在Python文档中非正式地命名为“reflected”)。因此,有两种方法可以计算比较运算 x 运算符 y 的结果:调用 x.__operator__(y) 或 y.__operatorT__(x)< /代码>。 Python 使用以下计算策略:
x
和y
不受支持(由返回值NotImplemented
指示),它将调用相反的特殊比较方法作为第一个后备。x
和y
不受支持(由返回值NotImplemented
指示),则会引发异常TypeError
除了比较运算符==
和!=
之外,它分别比较操作数x
和的同一性和非同一性y
作为第二个后备(利用==
的自反性属性和!=
的非自反性属性)。在 CPython 中 这是用 C 代码实现的,可以翻译成 Python 代码(
==
的名称为eq
,!=
的名称为ne
>、lt
表示<
、gt
表示>
、le
表示 < code><= 和ge
(对于>=
):由于 R = Ø(ØR),比较xRy相当于补比较 Ø(xØRy)。 ≠ 是= 的补码,因此默认情况下,特殊方法
__ne__
是根据支持的操作数的特殊方法__eq__
实现的,而其他特殊比较方法则独立实现默认情况下(事实上 ≤ 是 < 和 = 的并集,≥ 是 > 和 = 的并集 令人惊讶的是没有考虑,这意味着目前特殊方法__le__
和 < code>__ge__ 应该由用户实现),如 Python 文档:在 CPython 中 这是用 C 代码实现的,可以翻译成Python代码:
所以默认情况下:
x运算符y
会引发异常TypeError
,比较运算符==<除外/code> 和
分别为相同和不同,否则值为!=
如果操作数x
和 <,则分别返回值True
和False
code>yFalse
和True
;x.__operator__(y)
返回值NotImplemented
,特殊比较方法__eq__
和__ne__
除外如果操作数x
和y
分别相同,则它分别返回值True
和False
不相同,否则值为NotImplemented
。Correct implementation of
__ne__
@ShadowRanger’s implementation of the special method
__ne__
is the correct one:It also happens to be the default implementation of the special method
__ne__
since Python 3.4, as stated in the Python documentation:Also note that returning the value
NotImplemented
for unsupported operands is not specific to the special method__ne__
. In fact, all the special comparison methods1 and special numeric methods2 should return the valueNotImplemented
for unsupported operands, as specified in the Python documentation:An example for the special numeric methods is given in the Python documentation:
1 The special comparison methods:
__lt__
,__le__
,__eq__
,__ne__
,__gt__
and__ge__
.2 The special numeric methods:
__add__
,__sub__
,__mul__
,__matmul__
,__truediv__
,__floordiv__
,__mod__
,__divmod__
,__pow__
,__lshift__
,__rshift__
,__and__
,__xor__
,__or__
and their__r*__
reflected and__i*__
in-place counterparts.Incorrect implementation of
__ne__
#1@Falmarri’s implementation of the special method
__ne__
is incorrect:The problem with this implementation is that it does not fall back on the special method
__ne__
of the other operand as it never returns the valueNotImplemented
(the expressionnot self.__eq__(other)
evaluates to the valueTrue
orFalse
, including when its subexpressionself.__eq__(other)
evaluates to the valueNotImplemented
since the expressionbool(NotImplemented)
evaluates to the valueTrue
). The Boolean evaluation of the valueNotImplemented
breaks the complement relationship between the comparison operators!=
and==
:Incorrect implementation of
__ne__
#2@AaronHall’s implementation of the special method
__ne__
is also incorrect:The problem with this implementation is that it directly falls back on the special method
__eq__
of the other operand, bypassing the special method__ne__
of the other operand as it never returns the valueNotImplemented
(the expressionnot self == other
falls back on the special method__eq__
of the other operand and evaluates to the valueTrue
orFalse
). Bypassing a method is incorrect because that method may have side effects like updating the state of the object:Understanding comparison operations
In mathematics, a binary relation R over a set X is a set of ordered pairs (x, y) in X2. The statement (x, y) in R reads ‘x is R-related to y’ and is denoted by xRy.
Properties of a binary relation R over a set X:
For example, =. However ≠ is only symmetric.
For example, ≤ and ≥.
For example, < and >. However ≠ is only irreflexive.
Operations on two binary relations R and S over a set X:
Relationships between comparison relations that are always valid:
Relationships between comparison relations that are only valid for connex order relations:
So to correctly implement in Python the comparison operators
==
,!=
,<
,>
,<=
, and>=
corresponding to the comparison relations =, ≠, <, >, ≤, and ≥, all the above mathematical properties and relationships should hold.A comparison operation
x operator y
calls the special comparison method__operator__
of the class of one of its operands:Since R is reflexive implies xRx, a reflexive comparison operation
x operator y
(x == y
,x <= y
andx >= y
) or reflexive special comparison method callx.__operator__(y)
(x.__eq__(y)
,x.__le__(y)
andx.__ge__(y)
) should evaluate to the valueTrue
ifx
andy
are identical, that is if the expressionx is y
evaluates toTrue
. Since R is irreflexive implies not xRx, an irreflexive comparison operationx operator y
(x != y
,x < y
andx > y
) or irreflexive special comparison method callx.__operator__(y)
(x.__ne__(y)
,x.__lt__(y)
andx.__gt__(y)
) should evaluate to the valueFalse
ifx
andy
are identical, that is if the expressionx is y
evaluates toTrue
. The reflexive property is considered by Python for the comparison operator==
and associated special comparison method__eq__
but surprisingly not considered for the comparison operators<=
and>=
and associated special comparison methods__le__
and__ge__
, and the irreflexive property is considered by Python for the comparison operator!=
and associated special comparison method__ne__
but surprisingly not considered for the comparison operators<
and>
and associated special comparison methods__lt__
and__gt__
. The ignored comparison operators instead raise the exceptionTypeError
(and associated special comparison methods instead return the valueNotImplemented
), as explained in the Python documentation:The class
object
provides the default implementations of the special comparison methods which are inherited by all its subclasses, as explained in the Python documentation:Since R = (RT)T, a comparison xRy is equivalent to the converse comparison yRTx (informally named ‘reflected’ in the Python documentation). So there are two ways to compute the result of a comparison operation
x operator y
: calling eitherx.__operator__(y)
ory.__operatorT__(x)
. Python uses the following computing strategy:x.__operator__(y)
unless the right operand’s class is a descendant of the left operand’s class, in which case it callsy.__operatorT__(x)
(allowing classes to override their ancestors’ converse special comparison method).x
andy
are unsupported (indicated by the return valueNotImplemented
), it calls the converse special comparison method as a 1st fallback.x
andy
are unsupported (indicated by the return valueNotImplemented
), it raises the exceptionTypeError
except for the comparison operators==
and!=
for which it compares respectively the identity and non-identity of the operandsx
andy
as a 2nd fallback (leveraging the reflexivity property of==
and irreflexivity property of!=
).In CPython this is implemented in C code, which can be translated into Python code (with the names
eq
for==
,ne
for!=
,lt
for<
,gt
for>
,le
for<=
andge
for>=
):Since R = ¬(¬R), a comparison xRy is equivalent to the complement comparison ¬(x¬Ry). ≠ is the complement of =, so the special method
__ne__
is implemented in terms of the special method__eq__
for supported operands by default, while the other special comparison methods are implemented independently by default (the fact that ≤ is the union of < and =, and ≥ is the union of > and = is surprisingly not considered, which means that currently the special methods__le__
and__ge__
should be user implemented), as explained in the Python documentation:In CPython this is implemented in C code, which can be translated into Python code:
So by default:
x operator y
raises the exceptionTypeError
except for the comparison operators==
and!=
for which it returns respectively the valuesTrue
andFalse
if the operandsx
andy
are respectively identical and non-identical, and the valuesFalse
andTrue
otherwise;x.__operator__(y)
returns the valueNotImplemented
except for the special comparison methods__eq__
and__ne__
for which it returns respectively the valuesTrue
andFalse
if the operandsx
andy
are respectively identical and non-identical, and the valueNotImplemented
otherwise.