在 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__ 的方法有什么特别的缺点吗?
注意:一点澄清——当 __eq__
和 __ne__
未定义时,您会发现以下行为:
>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False
即 a == b
的计算结果为 False
因为它确实运行了 a is b
,即身份测试(即“Is a
与 b
相同的对象?”)。
当定义 __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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
另一种支持等价的优雅方式是使用@dataclass。 您的
Foo
示例将变为:就是这样! 现在的行为如下:
如果您的类需要提供其他实例属性,而这些属性不应在等效性中发挥作用,则在 __post_init__ 中定义它们,如下所示:
Another elegant way of supporting equivalences, is to use
@dataclass
. YourFoo
example would then become:That's it! Now the behaviour is as follows:
If your class needs to provide other instance attributes, which should not play a role in the equivalence, then define them in
__post_init__
, like so:我编写了一个带有
__ne__
默认实现的自定义基类,它简单地否定了__eq__
:如果您从这个基类继承,则只需实现
__eq__
> 和底座。回想起来,更好的方法可能是将其实现为装饰器。 类似于
@functools.total_ordering
I wrote a custom base with a default implementation of
__ne__
that simply negates__eq__
:If you inherit from this base class, you only have to implement
__eq__
and the base.In retrospect, a better approach might have been to implement it as a decorator instead. Something like
@functools.total_ordering
考虑这个简单的问题:
因此,Python 默认情况下使用对象标识符进行比较操作:
重写 __eq__ 函数似乎可以解决问题:
在 Python 2 中,始终记住重写
__ne__
函数,以及文档< /a> 状态:在 Python 3 中,这不再是必需的,因为 文档指出:
但这并不能解决我们所有的问题。 让我们添加一个子类:
注意:Python 2 有两种类:
经典风格(或旧风格)类,不< /em> 继承自
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 == n3
和n3 == n1
都调用n3.__eq__;
n1 != n3
和n3 != n1
都调用n3.__ne__
。要修复 Python 2 经典样式类的
==
和!=
运算符的非交换性问题,请使用__eq__
和当不支持操作数类型时,__ne__
方法应返回NotImplemented
值。 文档定义了NotImplemented< /code> 值为:
在这种情况下,运算符将比较操作委托给其他操作数的反射方法。 文档将反射方法定义为:
结果如下所示:
返回
NotImplemented
值而不是False
是正确的做法,即使对于新式类,如果当操作数属于不相关类型(无继承)时,需要使用==
和!=
运算符。我们到了吗? 不完全的。 我们有多少个唯一的号码?
集合使用对象的哈希值,默认情况下 Python 返回对象标识符的哈希值。 让我们尝试覆盖它:
最终结果如下所示(我在最后添加了一些断言以进行验证):
Consider this simple problem:
So, Python by default uses the object identifiers for comparison operations:
Overriding the
__eq__
function seems to solve the problem:In Python 2, always remember to override the
__ne__
function as well, as the documentation states:In Python 3, this is no longer necessary, as the documentation states:
But that does not solve all our problems. Let’s add a subclass:
Note: Python 2 has two kinds of classes:
classic-style (or old-style) classes, that do not inherit from
object
and that are declared asclass A:
,class A():
orclass A(B):
whereB
is a classic-style class;new-style classes, that do inherit from
object
and that are declared asclass A(object)
orclass A(B):
whereB
is a new-style class. Python 3 has only new-style classes that are declared asclass A:
,class A(object):
orclass 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
callsn1.__eq__
;n3 == n1
callsn3.__eq__
;n1 != n3
callsn1.__ne__
;n3 != n1
callsn3.__ne__
.And if
Number
is a new-style class:n1 == n3
andn3 == n1
calln3.__eq__
;n1 != n3
andn3 != n1
calln3.__ne__
.To fix the non-commutativity issue of the
==
and!=
operators for Python 2 classic-style classes, the__eq__
and__ne__
methods should return theNotImplemented
value when an operand type is not supported. The documentation defines theNotImplemented
value as:In this case the operator delegates the comparison operation to the reflected method of the other operand. The documentation defines reflected methods as:
The result looks like this:
Returning the
NotImplemented
value instead ofFalse
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?
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:
The end result looks like this (I added some assertions at the end for validation):
您需要小心继承:
更严格地检查类型,如下所示:
除此之外,您的方法将正常工作,这就是特殊方法的用途。
You need to be careful with inheritance:
Check types more strictly, like this:
Besides that, your approach will work fine, that's what special methods are there for.
你所描述的方式是我一直以来的做法。 由于它是完全通用的,因此您始终可以将该功能分解为 mixin 类,并在需要该功能的类中继承它。
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.
这不是一个直接的答案,但似乎足够相关,可以补充,因为它有时可以节省一些冗长的乏味。 直接从文档中剪切...
functools.total_ordering(cls)
< strong>给定一个定义一个或多个丰富比较排序方法的类,该类装饰器提供其余的内容。这简化了指定所有可能的丰富比较操作所涉及的工作:
该类必须定义以下之一:__lt__ ()、
__le__()
、__gt__()
或__ge__()
。 此外,该类应该提供一个 __eq__() 方法。2.7版本新增
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
您不必同时覆盖
__eq__
和__ne__
您可以仅覆盖__cmp__
但这将对 == 的结果产生影响, !==, < ,> 等等。is
测试对象身份。 这意味着当 a 和 b 都持有对同一对象的引用时,ais
b 将为True
。 在Python中,您总是在变量中保存对对象的引用,而不是实际对象,因此本质上,对于a is b为真,它们中的对象应该位于相同的内存位置。 最重要的是,你为什么要重写这种行为?编辑:我不知道
__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 ais
b will beTrue
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.从这个答案: https://stackoverflow.com/a/30676267/541136 我已经证明了这一点,虽然这是正确的用
__eq__
定义__ne__
- 而不是你应该使用:
From this answer: https://stackoverflow.com/a/30676267/541136 I have demonstrated that, while it's correct to define
__ne__
in terms__eq__
- instead ofyou should use:
我认为您要查找的两个术语是平等 (==) 和身份 (is)。 例如:
I think that the two terms you're looking for are equality (==) and identity (is). For example:
“is”测试将使用内置的“id()”函数来测试身份,该函数本质上返回对象的内存地址,因此不可重载。
但是,在测试类的相等性的情况下,您可能希望对测试更加严格,并且仅比较类中的数据属性:
此代码将仅比较类的非函数数据成员并跳过任何私人的东西通常都是你想要的。 对于普通旧 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:
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.
我不喜欢使用子类化/混合,而是使用通用类装饰器
用法:
Instead of using subclassing/mixins, I like to use a generic class decorator
Usage:
这结合了对 Algorias 答案的评论,并通过单个属性比较对象,因为我不关心整个字典。
hasattr(other, "id")
必须为 true,但我知道这是因为我在构造函数中设置了它。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.