Should I implement __ne__ as the negation of __eq__ in Python?

后端 未结 5 1916
无人共我
无人共我 2020-11-27 11:55

I have a class where I want to override the __eq__ method. It seems to make sense that I should override the __ne__ method as well. Should I implem

5条回答
  •  暖寄归人
    2020-11-27 12:36

    Correct __ne__ implementation

    @ShadowRanger’s implementation of the special method __ne__ is the correct one:

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented
    

    It also happens to be the default implementation of the special method __ne__ since Python 3.4, as stated in the Python documentation:

    By default, __ne__() delegates to __eq__() and inverts the result unless it is NotImplemented.

    Also note that returning the value NotImplemented for unsupported operands is not specific to the special method __ne__. In fact, all the special comparison methods1 and special numeric methods2 should return the value NotImplemented for unsupported operands, as specified in the Python documentation:

    NotImplemented

    This type has a single value. There is a single object with this value. This object is accessed through the built-in name NotImplemented. Numeric methods and rich comparison methods should return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true.

    An example for the special numeric methods is given in the Python documentation:

    class MyIntegral(Integral):
    
        def __add__(self, other):
            if isinstance(other, MyIntegral):
                return do_my_adding_stuff(self, other)
            elif isinstance(other, OtherTypeIKnowAbout):
                return do_my_other_adding_stuff(self, other)
            else:
                return NotImplemented
    
        def __radd__(self, other):
            if isinstance(other, MyIntegral):
                return do_my_adding_stuff(other, self)
            elif isinstance(other, OtherTypeIKnowAbout):
                return do_my_other_adding_stuff(other, self)
            elif isinstance(other, Integral):
                return int(other) + int(self)
            elif isinstance(other, Real):
                return float(other) + float(self)
            elif isinstance(other, Complex):
                return complex(other) + complex(self)
            else:
                return NotImplemented
    

    1 The special comparison methods: __lt__, __le__, __eq__, __ne__, __gt__ and __ge__.

    2 The special numeric methods: __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__, __mod__, __divmod__, __pow__, __lshift__, __rshift__, __and__, __xor__, __or__ and their __r*__ reflected and __i*__ in-place counterparts.

    Incorrect __ne__ implementation #1

    @Falmarri’s implementation of the special method __ne__ is incorrect:

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

    The problem with this implementation is that it does not fall back on the special method __ne__ of the other operand as it never returns the value NotImplemented (the expression not self.__eq__(other) evaluates to the value True or False, including when its subexpression self.__eq__(other) evaluates to the value NotImplemented since the expression bool(NotImplemented) evaluates to the value True). The Boolean evaluation of the value NotImplemented breaks the complement relationship between the comparison operators != and ==:

    class Correct:
    
        def __ne__(self, other):
            result = self.__eq__(other)
            if result is not NotImplemented:
                return not result
            return NotImplemented
    
    
    class Incorrect:
    
        def __ne__(self, other):
            return not self.__eq__(other)
    
    
    x, y = Correct(), Correct()
    assert (x != y) is not (x == y)
    
    x, y = Incorrect(), Incorrect()
    assert (x != y) is not (x == y)  # AssertionError
    

    Incorrect __ne__ implementation #2

    @AaronHall’s implementation of the special method __ne__ is also incorrect:

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

    The problem with this implementation is that it directly falls back on the special method __eq__ of the other operand, bypassing the special method __ne__ of the other operand as it never returns the value NotImplemented (the expression not self == other falls back on the special method __eq__ of the other operand and evaluates to the value True or False). Bypassing a method is incorrect because that method may have side effects like updating the state of the object:

    class Correct:
    
        def __init__(self):
            self.counter = 0
    
        def __ne__(self, other):
            self.counter += 1
            result = self.__eq__(other)
            if result is not NotImplemented:
                return not result
            return NotImplemented
    
    
    class Incorrect:
    
        def __init__(self):
            self.counter = 0
    
        def __ne__(self, other):
            self.counter += 1
            return not self == other
    
    
    x, y = Correct(), Correct()
    assert x != y
    assert x.counter == y.counter
    
    x, y = Incorrect(), Incorrect()
    assert x != y
    assert x.counter == y.counter  # AssertionError
    

    Understanding comparison operations

    In mathematics, a binary relation R over a set X is a set of ordered pairs (xy) in X2. The statement (xy) in R reads "x is R-related to y" and is denoted by xRy.

    Properties of a binary relation R over a set X:

    • R is reflexive when for all x in X, xRx.
    • R is irreflexive (also called strict) when for all x in X, not xRx.
    • R is symmetric when for all x and y in X, if xRy then yRx.
    • R is antisymmetric when for all x and y in X, if xRy and yRx then x = y.
    • R is transitive when for all x, y and z in X, if xRy and yRz then xRz.
    • R is connex (also called total) when for all x and y in X, xRy or yRx.
    • R is an equivalence relation when R is reflexive, symmetric and transitive.
      For example, =. However ≠ is only symmetric.
    • R is an order relation when R is reflexive, antisymmetric and transitive.
      For example, ≤ and ≥.
    • R is a strict order relation when R is irreflexive, antisymmetric and transitive.
      For example, < and >. However ≠ is only irreflexive.

    Operations on two binary relations R and S over a set X:

    • The converse of R is the binary relation RT = {(yx) | xRy} over X.
    • The complement of R is the binary relation ¬R = {(xy) | not xRy} over X.
    • The union of R and S is the binary relation R ∪ S = {(xy) | xRy or xSy} over X.

    Relationships between comparison relations that are always valid:

    • 2 complementary relationships: = and ≠ are each other’s complement;
    • 6 converse relationships: = is the converse of itself, ≠ is the converse of itself, < and > are each other’s converse, and ≤ and ≥ are each other’s converse;
    • 2 union relationships: ≤ is the union < and =, and ≥ is the union of > and =.

    Relationships between comparison relations that are only valid for connex orders:

    • 4 complementary relationships: < and ≥ are each other’s complement, and > and ≤ are each other’s complement.

    So to correctly implement in Python the comparison operators ==, !=, <, >, <=, and >= corresponding to the comparison relations =, ≠, <, >, ≤, and ≥, all the above mathematical properties and relationships should hold.

    A comparison operation x operator y calls the special comparison method __operator__ of the class of one of its operands:

    class X:
    
        def __operator__(self, other):
            # implementation
    

    Since R is reflexive implies xRx, a reflexive comparison operation x operator y (x == y, x <= y and x >= y) or reflexive special comparison method call x.__operator__(y) (x.__eq__(y), x.__le__(y) and x.__ge__(y)) should evaluate to the value True if x and y are identical, that is if the expression x is y evaluates to True. Since R is irreflexive implies not xRx, an irreflexive comparison operation x operator y (x != y, x < y and x > y) or irreflexive special comparison method call x.__operator__(y) (x.__ne__(y), x.__lt__(y) and x.__gt__(y)) should evaluate to the value False if x and y are identical, that is if the expression x is y evaluates to True. The reflexive property is considered by Python for the comparison operator == and associated special comparison method __eq__ but surprisingly not considered for the comparison operators <= and >= and associated special comparison methods __le__ and __ge__, and the irreflexive property is considered by Python for the comparison operator != and associated special comparison method __ne__ but surprisingly not considered for the comparison operators < and > and associated special comparison methods __lt__ and __gt__. The ignored comparison operators instead raise the exception TypeError (and associated special comparison methods instead return the value NotImplemented), as explained in the Python documentation:

    The default behavior for equality comparison (== and !=) is based on the identity of the objects. Hence, equality comparison of instances with the same identity results in equality, and equality comparison of instances with different identities results in inequality. A motivation for this default behavior is the desire that all objects should be reflexive (i.e. x is y implies x == y).

    A default order comparison (<, >, <=, and >=) is not provided; an attempt raises TypeError. A motivation for this default behavior is the lack of a similar invariant as for equality. [This is incorrect since <= and >= are reflexive like ==, and < and > are irreflexive like !=.]

    The class object provides the default implementations of the special comparison methods which are inherited by all its subclasses, as explained in the Python documentation:

    object.__lt__(self, other)
    object.__le__(self, other)
    object.__eq__(self, other)
    object.__ne__(self, other)
    object.__gt__(self, other)
    object.__ge__(self, other)

    These are the so-called “rich comparison” methods. The correspondence between operator symbols and method names is as follows: x calls x.__lt__(y), x<=y calls x.__le__(y), x==y calls x.__eq__(y), x!=y calls x.__ne__(y), x>y calls x.__gt__(y), and x>=y calls x.__ge__(y).

    A rich comparison method may return the singleton NotImplemented if it does not implement the operation for a given pair of arguments.

    […]

    There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection. If the operands are of different types, and right operand’s type is a direct or indirect subclass of the left operand’s type, the reflected method of the right operand has priority, otherwise the left operand’s method has priority. Virtual subclassing is not considered.

    Since R = (RT)T, a comparison xRy is equivalent to the converse comparison yRTx (informally named "reflected" in the Python documentation). So there are two ways to compute the result of a comparison operation x operator y: calling either x.__operator__(y) or y.__operatorT__(x). Python uses the following computing strategy:

    1. It calls x.__operator__(y) unless the right operand’s class is a descendant of the left operand’s class, in which case it calls y.__operatorT__(x) (allowing classes to override their ancestors’ converse special comparison method).
    2. If the operands x and y are unsupported (indicated by the return value NotImplemented), it calls the converse special comparison method as a 1st fallback.
    3. If the operands x and y are unsupported (indicated by the return value NotImplemented), it raises the exception TypeError except for the comparison operators == and != for which it tests respectively the identity and non-identity of the operands x and y as a 2nd fallback (leveraging the reflexivity property of == and irreflexivity property of !=).
    4. It returns the result.

    In CPython this is implemented in C code, which can be translated into Python code (with the names eq for ==, ne for !=, lt for <, gt for >, le for <= and ge for >=):

    def eq(left, right):
        if type(left) != type(right) and isinstance(right, type(left)):
            result = right.__eq__(left)
            if result is NotImplemented:
                result = left.__eq__(right)
        else:
            result = left.__eq__(right)
            if result is NotImplemented:
                result = right.__eq__(left)
        if result is NotImplemented:
            result = left is right
        return result
    
    def ne(left, right):
        if type(left) != type(right) and isinstance(right, type(left)):
            result = right.__ne__(left)
            if result is NotImplemented:
                result = left.__ne__(right)
        else:
            result = left.__ne__(right)
            if result is NotImplemented:
                result = right.__ne__(left)
        if result is NotImplemented:
            result = left is not right
        return result
    
    def lt(left, right):
        if type(left) != type(right) and isinstance(right, type(left)):
            result = right.__gt__(left)
            if result is NotImplemented:
                result = left.__lt__(right)
        else:
            result = left.__lt__(right)
            if result is NotImplemented:
                result = right.__gt__(left)
        if result is NotImplemented:
            raise TypeError(
                f"'<' not supported between instances of '{type(left).__name__}' "
                f"and '{type(right).__name__}'"
            )
        return result
    
    def gt(left, right):
        if type(left) != type(right) and isinstance(right, type(left)):
            result = right.__lt__(left)
            if result is NotImplemented:
                result = left.__gt__(right)
        else:
            result = left.__gt__(right)
            if result is NotImplemented:
                result = right.__lt__(left)
        if result is NotImplemented:
            raise TypeError(
                f"'>' not supported between instances of '{type(left).__name__}' "
                f"and '{type(right).__name__}'"
            )
        return result
    
    def le(left, right):
        if type(left) != type(right) and isinstance(right, type(left)):
            result = right.__ge__(left)
            if result is NotImplemented:
                result = left.__le__(right)
        else:
            result = left.__le__(right)
            if result is NotImplemented:
                result = right.__ge__(left)
        if result is NotImplemented:
            raise TypeError(
                f"'<=' not supported between instances of '{type(left).__name__}' "
                f"and '{type(right).__name__}'"
            )
        return result
    
    def ge(left, right):
        if type(left) != type(right) and isinstance(right, type(left)):
            result = right.__le__(left)
            if result is NotImplemented:
                result = left.__ge__(right)
        else:
            result = left.__ge__(right)
            if result is NotImplemented:
                result = right.__le__(left)
        if result is NotImplemented:
            raise TypeError(
                f"'>=' not supported between instances of '{type(left).__name__}' "
                f"and '{type(right).__name__}'"
            )
        return result
    

    Since R = ¬(¬R), a comparison xRy is equivalent to the complement comparison ¬(x¬Ry). ≠ is the complement of =, so the special method __ne__ is implemented in terms of the special method __eq__ for supported operands by default, while the other special comparison methods are implemented independently by default (the fact that ≤ is the union of < and =, and ≥ is the union of > and = is surprisingly not considered, which means that currently the special methods __le__ and __ge__ should be user implemented), as explained in the Python documentation:

    By default, __ne__() delegates to __eq__() and inverts the result unless it is NotImplemented. There are no other implied relationships among the comparison operators, for example, the truth of (x does not imply x<=y.

    In CPython this is implemented in C code, which can be translated into Python code:

    def __eq__(self, other):
        return self is other or NotImplemented
    
    def __ne__(self, other):
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented
    
    def __lt__(self, other):
        return NotImplemented
    
    def __gt__(self, other):
        return NotImplemented
    
    def __le__(self, other):
        return NotImplemented
    
    def __ge__(self, other):
        return NotImplemented
    

    So by default:

    • a comparison operation x operator y raises the exception TypeError except for the comparison operators == and != for which it returns respectively the identity and non-identity of the operands x and y;
    • a special comparison method call x.__operator__(y) returns the value NotImplemented except for the special comparison methods __eq__ and __ne__ for which it returns respectively True and False if the operands x and y are respectively identical and non-identical and the value NotImplemented otherwise.

提交回复
热议问题