首页 文章

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

提问于
浏览
320

编写自定义类时,通过 ==!= 运算符允许等效通常很重要 . 在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__ 的方法有什么特别的缺点?

Note :有点澄清 - 当 __eq____ne__ 未定义时,你会发现这种行为:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

也就是说, a == b 计算为 False ,因为它确实运行 a is b ,一个身份测试(即“ ab 相同的对象?”) .

__eq____ne__ 被定义时,你'll find this behavior (which is the one we'之后):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True

8 回答

  • 13

    考虑这个简单的问题:

    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__ 函数,因为documentation表示:

    比较运营商之间没有隐含的关系 . 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中,这不再是必需的,因为documentation指出:

    默认情况下,__ ne __()委托给__eq __()并反转结果,除非它是NotImplemented . 比较运算符之间没有其他隐含的关系,例如,(x <y或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
    

    Note: Python 2有两种类:

    • classic-style(或旧式)类,不从 object 继承,并且声明为 class A:class A():class A(B): ,其中 B 是经典风格的类;

    • new-style类,继承自 object 并声明为 class A(object)class A(B): ,其中 B 是新式类 . Python 3只有新式的类,声明为 class A:class A(object):class A(B): .

    对于经典样式类,比较操作总是调用第一个操作数的方法,而对于新样式类,它总是调用子类操作数regardless of the order of the operands的方法 .

    所以在这里,如果 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 值 . documentationNotImplemented 值定义为:

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

    在这种情况下,运算符将比较操作委托给另一个操作数的反射方法 . documentation将反射方法定义为:

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

    你需要小心继承:

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

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

  • 3

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

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


    functools.total_ordering(cls)

    Given a class defining one or more rich comparison ordering methods, this class decorator supplies the rest. 这简化了指定所有可能的丰富比较操作所涉及的工作:

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

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

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

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

  • 154

    从这个回答:https://stackoverflow.com/a/30676267/541136我已经证明,虽然用 __eq__ 定义 __ne__ 是正确的 - 而不是

    def __ne__(self, other):
        return not self.__eq__(other)
    

    你应该使用:

    def __ne__(self, other):
        return not self == other
    
  • 184

    我认为您正在寻找的两个术语是 equality (==)和 identity (是) . 例如:

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

    '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
    

    此代码仅比较您的类的非函数数据成员以及跳过任何私有的,这通常是您想要的 . 在Plain Old Python Objects的情况下,我有一个基类,它实现了__init ttr __,repr__和__eq,因此我的POPO对象不承担所有额外(在大多数情况下是相同的)逻辑的负担 .

相关问题