当前位置:  开发笔记 > 编程语言 > 正文

在Python类中支持等价("相等")的优雅方法

如何解决《在Python类中支持等价("相等")的优雅方法》经验,为你挑选了6个好方法。

在编写自定义类时,通过==!=运算符允许等效通常很重要.在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

Tal Weiss.. 289

考虑这个简单的问题:

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并非暗示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 == n3n3 == n1调用n3.__eq__;

两个n1 != n3n3 != n1打电话n3.__ne__.

为了修复Python 2经典样式类==!=运算符的非交换性问题,当不支持操作数类型时,__eq____ne__方法应该返回NotImplemented值.该文档定义了NotImplemented值为:

如果数值方法和丰富的比较方法未实现所提供操作数的操作,则它们可能会返回此值.(然后,解释器将尝试反射操作或其他一些后备操作,具体取决于操作员.)其真值是真的.

在这种情况下操作者的代表的比较操作的反射的方法的的其他操作数.该文档定义反射方法为:

这些方法没有交换参数版本(当左参数不支持操作但右参数支持时使用); 相反,__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 not NotImplemented:
        return not x
    return NotImplemented

如果操作数是不相关的类型(没有继承),那么如果需要和运算符的交换,那么返回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

很棒的总结,但你[应该使用`==`而不是`__eq__`实现`__ne__`](/sf/ask/17360801/). (11认同)

`hash(tuple(sorted(self .__ dict __.items())))`如果在`self .__ dict__`的值中有任何不可清除的对象,那么`将不起作用(即,如果有任何属性)对象设置为,例如,`list`). (3认同)

是的,但是如果你的vars()中有这样的可变对象,那么这两个对象并不是真的相等...... (3认同)

他问起优雅,但他变得健壮。 (2认同)


Algorias.. 195

你需要小心继承:

>>> 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

除此之外,您的方法将正常工作,这就是特殊方法.



1> Tal Weiss..:

考虑这个简单的问题:

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并非暗示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 == n3n3 == n1调用n3.__eq__;

两个n1 != n3n3 != n1打电话n3.__ne__.

为了修复Python 2经典样式类==!=运算符的非交换性问题,当不支持操作数类型时,__eq____ne__方法应该返回NotImplemented值.该文档定义了NotImplemented值为:

如果数值方法和丰富的比较方法未实现所提供操作数的操作,则它们可能会返回此值.(然后,解释器将尝试反射操作或其他一些后备操作,具体取决于操作员.)其真值是真的.

在这种情况下操作者的代表的比较操作的反射的方法的的其他操作数.该文档定义反射方法为:

这些方法没有交换参数版本(当左参数不支持操作但右参数支持时使用); 相反,__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 not NotImplemented:
        return not x
    return NotImplemented

如果操作数是不相关的类型(没有继承),那么如果需要和运算符的交换,那么返回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


很棒的总结,但你[应该使用`==`而不是`__eq__`实现`__ne__`](/sf/ask/17360801/).
`hash(tuple(sorted(self .__ dict __.items())))`如果在`self .__ dict__`的值中有任何不可清除的对象,那么`将不起作用(即,如果有任何属性)对象设置为,例如,`list`).
是的,但是如果你的vars()中有这样的可变对象,那么这两个对象并不是真的相等......
他问起优雅,但他变得健壮。

2> Algorias..:

你需要小心继承:

>>> 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,将比较委托给rhs.
@max比较不一定是左手边(LHS)到右手边(RHS),然后是RHS到LHS; 见http://stackoverflow.com/a/12984987/38140.仍然,按照你的建议返回`NotImplemented`总会导致`superclass .__ eq __(subclass)`,这是所需的行为.
如果你有大量的成员,并且周围没有很多对象副本,那么通常很好地添加一个初始身份测试`如果其他是自我.这避免了更长的字典比较,并且当对象用作字典键时可以节省大量资金.
并且不要忘记实现`__hash __()`

3> cdleary..:

你描述的方式是我一直以来的方式.由于它完全是通用的,因此您可以始终将该功能分解为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


`__dict__`比较的另一个问题是,如果你有一个你不想在你的相等定义中考虑的属性(比如说一个唯一的对象id,或者像时间创建的图章那样的元数据).
+1:策略模式,允许在子类中轻松替换.
实在很糟糕.为什么检查一下?为什么不只是自我.__ dict__ ==其他.__ dict__?
@nosklo:我不明白..如果来自完全不相关的类的两个对象碰巧具有相同的属性呢?

4> John Mee..:

这不是一个直接的答案,但似乎有足够的相关性,因为它有时会节省一些冗长的单调乏味.直接从文档中删除...


functools.total_ordering(CLS)

给定一个定义一个或多个丰富的比较排序方法的类,这个类装饰器提供其余的.这简化了指定所有可能的丰富比较操作所涉及的工作:

该类必须定义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()))



5> Vasil..:

您不必覆盖它们__eq__,__ne__只能覆盖,__cmp__但这会对==,!==,<,>等结果产生影响.

is测试对象身份.这意味着当a和b都保持对同一对象的引用时,isb将True处于这种情况.在python中,你始终对变量中的对象而不是实际对象进行引用,因此对于a来说,a b是真的,它们中的对象应该位于同一个内存位置.你最重要的是为什么要重写这种行为?

编辑:我不知道__cmp__是从python 3中删除所以避免它.


在Python 3中,"cmp()函数消失了,不再支持__cmp __()特殊方法." http://is.gd/aeGv

6> Aaron Hall..:

从这个答案: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

推荐阅读
贾志军
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有