osdir.com


[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Why does __ne__ exist?


On Mon, Jan 8, 2018 at 7:13 AM, Thomas Jollans <tjol at tjol.eu> wrote:
> On 07/01/18 20:55, Chris Angelico wrote:
>> Under what circumstances would you want "x != y" to be different from
>> "not (x == y)" ?
>
> In numpy, __eq__ and __ne__ do not, in general, return bools.
>
>>>> a = np.array([1,2,3,4])
>>>> b = np.array([0,2,0,4])
>>>> a == b
> array([False,  True, False,  True], dtype=bool)
>>>> a != b
> array([ True, False,  True, False], dtype=bool)

Thanks, that's the kind of example I was looking for. Though numpy
doesn't drive the core language development much, so the obvious next
question is: was this why __ne__ was implemented, or was there some
other reason? This example shows how it can be useful, but not why it
exists.

>>>> not (a == b)
> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
> ValueError: The truth value of an array with more than one element is
> ambiguous. Use a.any() or a.all()

Which means that this construct is still never going to come up in good code.

>>>> ~(a == b)
> array([ True, False,  True, False], dtype=bool)
>>>>
>
> I couldn't tell you why this was originally allowed, but it does turn
> out to be strangely useful. (As far as the numpy API is concerned, it
> would be even nicer if 'not' could be overridden, IMHO)

I'm glad 'not' can't be overridden; it'd be too hard to reason about a
piece of code if even the basic boolean operations could change. If
you want overridables, you have &|~ for the bitwise operators (which
is how numpy does things).

Has numpy ever asked for a "not in" dunder method (__not_contained__
or something)? It's a strange anomaly that "not (x in y)" can be
perfectly safely optimized to "x not in y", yet basic equality has to
have separate handling. The default handling does mean that you can
mostly ignore __ne__ and expect things to work, but if you subclass
something that has both, it'll break:

class Foo:
    def __eq__(self, other):
        print("Foo: %s == %s" % (self, other))
        return True
    def __ne__(self, other):
        print("Foo: %s != %s" % (self, other))
        return False

class Bar(Foo):
    def __eq__(self, other):
        print("Bar: %s == %s" % (self, other))
        return False

>>> Bar() == 1
Bar: <__main__.Bar object at 0x7f40ebf3a128> == 1
False
>>> Bar() != 1
Foo: <__main__.Bar object at 0x7f40ebf3a128> != 1
False

Obviously this trivial example looks stupid, but imagine if the
equality check in the subclass USUALLY gives the same result as the
superclass, but differs in rare situations. Maybe you create a "bag"
class that functions a lot like collections.Counter but ignores zeroes
when comparing:

>>> class Bag(collections.Counter):
...     def __eq__(self, other):
...         # Disregard any zero entries when comparing to another Bag
...         return {k:c for k,c in self.items() if c} == {k:c for k,c
in other.items() if c}
...
>>> b1 = Bag("aaabccdd")
>>> b2 = Bag("aaabccddq")
>>> b2["q"] -= 1
>>> b1 == b2
True
>>> b1 != b2
True
>>> dict(b1) == dict(b2)
False
>>> dict(b1) != dict(b2)
True

The behaviour of __eq__ is normal and sane. But since there's no
__ne__, the converse comparison falls back on dict.__ne__, not on
object.__ne__.

ChrisA