在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
,一个标识的testing(即“是a
与b
相同的对象吗?”)。
当__eq__
和__ne__
被定义时,你会发现这个行为(这是我们之后的行为):
>>> a = Foo(1) >>> b = Foo(1) >>> a is b False >>> a == b True
对不起,但没有答案完全“工作”。 考虑这个简单的问题:
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(self, other.__class__): return self.__dict__ == other.__dict__ 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
inheritance的class A:
,声明为class A:
class A():
或class A(B):
其中B
是一个经典风格的类和新风格的类inheritance自object
,声明为class A(object)
或class A(B):
其中B
是一个新风格的类。 Python 3只有新的类 – 声明为class A:
class A(object):
或class A(B):
对于新式类,比较操作调用子类操作数的方法,而不考虑操作数的顺序(所以这里n1 == n3
和n3 == n1
调用n3.__eq__
,而n1 != n3
和n3 != n1
call n3.__ne__
)。
要修复Python 2经典风格类的==
和!=
运算符的非交换性问题,当不支持操作数types时, __ne__
和__ne__
方法应返回NotImplemented
值。 该文档将NotImplemented
值定义为 :
数字方法和丰富的比较方法可能会返回这个值,如果他们没有实现提供的操作数的操作。 (解释者然后将根据操作员尝试reflection的操作或其他一些后备操作。)它的真值是正确的。
在这种情况下,操作员将比较结果委托给另一个操作数的reflection方法 。 文档定义反映的方法为 :
这些方法没有交换参数的版本(当左边的参数不支持这个操作,但是正确的参数是这样的)。 相反,
__lt__()
和__gt__()
是彼此的reflection,__le__()
和__ge__()
是彼此的reflection,__eq__()
__ne__()
和__ne__()
是他们自己的reflection。
结果如下所示:
def __eq__(self, other): """Overrides the default implementation""" if isinstance(self, other.__class__): return self.__dict__ == other.__dict__ 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
如果操作数是不相关的types(无inheritance)时需要==
和!=
运算符的交换性 ,则返回NotImplemented
值而不是False
即使对于新样式类也是正确的。
我们到了吗? 不完全的。 我们有多less个独特的号码?
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
最终的结果是这样的(我最后添加了一些断言进行validation):
class Number: def __init__(self, number): self.number = number def __eq__(self, other): """Overrides the default implementation""" if isinstance(self, other.__class__): return self.__dict__ == other.__dict__ 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()))) n1 = Number(1) n2 = Number(1) class SubNumber(Number): pass 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
你需要小心inheritance:
>>> 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
更严格地检查types,如下所示:
def __eq__(self, other): if type(other) is type(self): return self.__dict__ == other.__dict__ return False
除此之外,你的方法可以正常工作,那是什么特别的方法。
你描述的方式是我一直这样做的方式。 因为它是完全通用的,所以你总是可以把这个function分解成一个mixin类,并在需要这个function的类中inheritance它。
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
不是一个直接的答案,但似乎有足够的重要性,因为它有时节省了一点冗长乏味。 从文档中直接剪切…
functools.total_ordering(CLS)
给定一个定义一个或多个比较sorting方法的类,这个类装饰器提供剩下的部分。 这简化了指定所有可能的丰富比较操作的工作:
该类必须定义lt (), le (), gt ()或ge ()之一。 另外,这个类应该提供一个eq ()方法。
2.7版本中的新function
@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()))
您不必覆盖__eq__
和__ne__
,只能覆盖__cmp__
但这会影响==,!==,<,>等的结果。
is
testing对象身份。 这意味着当a和b同时持有对同一个对象的引用时,a是b。 在python中,你总是持有对一个variables中的对象的引用,而不是实际的对象,所以基本上对于a是b是真实的,它们中的对象应该位于相同的内存位置。 如何以及最重要的是你为什么要重写这种行为呢?
编辑:我不知道__cmp__
从Python 3中删除,所以要避免它。
从这个答案: https : __ne__
我已经certificate,虽然这是正确的定义__ne__
用__eq__
– 而不是
def __ne__(self, other): return not self.__eq__(other)
你应该使用:
def __ne__(self, other): return not self == other
我认为你正在寻找的两个术语是平等 (==)和身份 (是)。 例如:
>>> 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
'is'testing将使用内build的'id()'函数来testing身份,该函数本质上返回对象的内存地址,因此不能重载。
然而,在testing一个类的平等的情况下,你可能想对你的testing更严格些,只比较你的类中的数据属性:
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对象不承担所有额外的(并且在大多数情况下是相同的)逻辑的负担。