问题:在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__
s 比较方法有什么特别的缺点吗?
注意:需要澄清一点-当__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
回答 0
考虑这个简单的问题:
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
;新样式类,那些从继承
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
值定义为:
如果数字方法和丰富比较方法未实现所提供操作数的操作,则可能返回此值。(然后,解释程序将根据操作员尝试执行反射操作或其他回退。)其真实值是true。
在这种情况下操作者的代表的比较操作的反射的方法的的其他操作数。该文档将反映的方法定义为:
这些方法没有交换参数版本(当左参数不支持该操作但右参数支持该操作时使用);相反,
__lt__()
and__gt__()
是彼此的反射,__le__()
and__ge__()
是彼此的反射,and__eq__()
and__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
如果操作数是不相关的类型(无继承),如果需要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
回答 1
您需要小心继承:
>>> 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
除此之外,您的方法会很好地工作,这就是专用方法的目的。
回答 2
您描述的方式就是我一直以来所做的方式。由于它是完全通用的,因此您始终可以将该功能分解为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
回答 3
这不是一个直接的答案,但似乎足够相关,可以解决,因为它有时可以节省一些冗长的乏味。从文档中直接切出…
给定一个定义了一个或多个丰富比较排序方法的类,此类装饰器将提供其余的类。这简化了指定所有可能的丰富比较操作所涉及的工作:
这个类必须定义之一__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()))
回答 4
您不必覆盖两者,__eq__
而__ne__
只能覆盖,__cmp__
但这将对==,!==,<,>等结果产生影响。
is
测试对象身份。这意味着,当a和b都持有对同一对象的引用时,is
b就会出现True
。在python中,您始终会在变量中持有对对象的引用,而不是实际对象,因此从本质上来说,如果a为b为true,则其中的对象应位于相同的内存位置。最重要的是,为什么您要继续压倒这种行为?
编辑:我不知道__cmp__
从python 3中删除了,所以避免它。
回答 5
从这个答案: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
回答 6
我认为您要查找的两个术语是相等(==)和同一性(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
回答 7
“ 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对象不承担所有额外(在大多数情况下相同)逻辑的负担。
回答 8
我喜欢使用泛型类装饰器,而不是使用子类/混合器
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
回答 9
这合并了对Algorias答案的评论,并通过单个属性比较对象,因为我不在乎整个字典。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