提问人: 提问时间:12/24/2008 最后编辑:4 revs, 2 users 100%gotgenes 更新时间:7/14/2023 访问量:314154
在 Python 类中支持等价(“相等”)的优雅方式
Elegant ways to support equivalence ("equality") in Python classes
问:
在编写自定义类时,通过 and 运算符允许等效通常很重要。在 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
a
b
定义 和 后,您会发现此行为(这就是我们所追求的行为):__eq__
__ne__
>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
答:
您不必同时覆盖两者,只能覆盖,但这会影响 ==、!==、<、> 等的结果。__eq__
__ne__
__cmp__
is
测试对象标识。这意味着当 a 和 b 都持有对同一对象的引用时,a b 将处于 b 状态。在 python 中,你总是在变量中保存对对象的引用,而不是实际对象,所以本质上要使 a 是 b 为真,其中的对象应该位于相同的内存位置。你如何,最重要的是,你为什么要推翻这种行为?is
True
编辑:我不知道已从 python 3 中删除,因此请避免使用它。__cmp__
评论
我认为你要找的两个术语是平等(==)和身份(is)。例如:
>>> 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
评论
你描述的方式是我一直以来的做法。由于它是完全通用的,因此您始终可以将该功能分解为一个 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
评论
self
__eq__
True
__dict__
您需要注意继承:
>>> 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
除此之外,您的方法将正常工作,这就是特殊方法的用途。
评论
NotImplemented
superclass.__eq__(subclass)
if other is self
__hash__()
“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
此代码将仅比较类中的非函数数据成员,并跳过通常所需的任何私有内容。对于普通的旧 Python 对象,我有一个基类,它实现了 __init__、__str__、__repr__ 和 __eq__,所以我的 POPO 对象不会承担所有额外(在大多数情况下是相同的)逻辑的负担。
评论
__eq__
CommonEqualityMixin
_sa_instance_state
key.startswith("__")):
key.startswith("_")):
'_'
iteritems()
items()
__dict__
__
__class__
__init__
__dict__
__dict__
__
__eq__
__
这不是一个直接的答案,但似乎足够相关,因为它有时会节省一些冗长乏味。直接从文档中剪切...
给定一个类,定义一个或多个丰富的比较排序方法,该类装饰器提供其余部分。这简化了指定所有可能的丰富比较操作所涉及的工作:
该类必须定义 、 、 或 之一。此外,该类应提供方法。__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()))
评论
考虑这个简单的问题:
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__
比较运算符之间没有隐含关系。这 的真相并不意味着这是错误的。因此,当 定义,还应该定义,以便 操作员将按预期运行。
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 中,这不再是必需的,正如文档所述:
默认情况下,委托和反转结果 除非是.没有其他暗示 比较运算符之间的关系,例如,真值 的并不意味着 。
__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 有两种类:
经典样式(或旧样式)类,不继承自 ,并且声明为 ,或者 其中是经典样式类;
object
class A:
class A():
class A(B):
B
新样式类,这些类继承自新样式类,并且声明为 WHERE 是新样式类。Python 3 只有声明为 或 的新式类。
object
class A(object)
class A(B):
B
class A:
class A(object):
class A(B):
对于经典样式的类,比较操作始终调用第一个操作数的方法,而对于新样式的类,它始终调用子类操作数的方法,而不考虑操作数的顺序。
所以在这里,if 是一个经典风格的类: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 经典风格类的 and 运算符的非交换性问题,当操作数类型不受支持时,and 方法应返回该值。文档将该值定义为:==
!=
__eq__
__ne__
NotImplemented
NotImplemented
如果出现以下情况,数值方法和丰富的比较方法可能会返回此值 它们不对提供的操作数实现操作。( 然后,解释器将尝试反射操作,或其他一些操作 回退,具体取决于操作员。它的真值是真的。
在这种情况下,运算符将比较操作委托给另一个操作数的反射方法。该文档将反射的方法定义为:
这些方法没有交换参数版本(要使用 当 left 参数不支持操作但 right 参数时 论点确实);相反,并且是彼此的 反思,是彼此的反思,也是自己的反思。
__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 NotImplemented:
return NotImplemented
return not x
返回值而不是 of 是正确的做法,即使对于新式类,如果当操作数是不相关的类型(无继承)时,需要 and 运算符的可交换性。NotImplemented
False
==
!=
我们到了吗?差一点。我们有多少个唯一号码?
len(set([n1, n2, n3])) # 3 -- oops
集合使用对象的哈希值,默认情况下,Python 返回对象标识符的哈希值。让我们尝试覆盖它:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
最终结果如下所示(我在末尾添加了一些断言以进行验证):
class Number:
def __init__(self, number):
self.number = number
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
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
class SubNumber(Number):
pass
n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)
assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1
assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1
assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1
assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
评论
hash(tuple(sorted(self.__dict__.items())))
如果 的值中有任何不可哈希的对象(即,如果对象的任何属性设置为,例如,一个)。self.__dict__
list
==
而不是 __eq__
来实现__ne__
。
n1 == n3
甚至应该为经典类吗?因为这种情况应该是并且是正确的?True
other
n3
isinstance(n3, Number)
从这个答案: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
我喜欢使用通用类装饰器,而不是使用子类/mixin
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
这包含了对 Algoria 答案的评论,并按单个属性比较对象,因为我不关心整个字典。 必须是真的,但我知道这是因为我在构造函数中设置了它。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
我编写了一个自定义库,其默认实现只是否定:__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
的东西
支持等价的另一种优雅方式是使用 .然后,您的示例将变为:@dataclass
Foo
from dataclasses import dataclass
@dataclass
class Foo:
item: int
就是这样!现在行为如下:
a = Foo(1)
b = Foo(1)
print(a == b) # True
c = Foo(2)
print(a == c) # False
如果您的类需要提供其他实例属性,这些属性不应在等价性中发挥作用,则在 中定义它们,如下所示:__post_init__
from dataclasses import dataclass
from random import randint
@dataclass
class Foo:
age: int
name: str
def __post_init__(self):
self.rnd = randint(1, 100000)
a = Foo(38, "Helen")
b = Foo(38, "Helen")
print(a == b) # True
print(a.rnd == b.rnd) # False, probably ;-)
评论
is