9

I have a little helper class:

class AnyOf(object):
    def __init__(self, *args):
        self.elements = args
    def __eq__(self, other):
        return other in self.elements

This lets me do sweet magic like:

>>> arr = np.array([1,2,3,4,5])
>>> arr == AnyOf(2,3)
np.array([False, True, True, False, False])

without having to use a list comprehension (as in np.array(x in (2,3) for x in arr).

(I maintain a UI that lets (trusted) users type in arbitrary code, and a == AnyOf(1,2,3) is a lot more palatable than a list comprehension to the non-technically savvy user.)

However!

This only works one way! For example, if I were to do AnyOf(2,3) == arr then my AnyOf class's __eq__ method never gets called: instead, the NumPy array's __eq__ method gets called, which internally (I would presume) calls the __eq__ method of all its elements.

This lead me to wonder: why does Python not allow a right-sided equivalent to __eq__? (Roughly equivalent to methods like __radd__, __rmul__, et cetera.)

wim
  • 338,267
  • 99
  • 616
  • 750
acdr
  • 4,538
  • 2
  • 19
  • 45
  • 2
    "Why does Python not have X?" isn't likely to be a very productive question. If you want Python to have X, the productive way to make that happen is to follow [the process in PEP-1](https://www.python.org/dev/peps/pep-0001/#start-with-an-idea-for-python) to actually propose the idea and get feedback from the people who have a say on what gets added to the language. Otherwise, "Is there a way to get the useful effect of X in Python?" is much more likely to actually have an immediately useful result (wrt. generating light rather than heat). – Charles Duffy Nov 30 '17 at 15:59
  • 1
    Related: https://stackoverflow.com/questions/3588776/how-is-eq-handled-in-python-and-in-what-order – Craig Nov 30 '17 at 16:00
  • Actually I can't recognize an urgent need for a class in the example above. Why not use a simple function, which returns a list? – guidot Nov 30 '17 at 16:01
  • 1
    Note that if you have somewhat large arrays this "magic" is going to be much slower than something like `np.in1d`. – miradulo Nov 30 '17 at 16:07
  • 2
    Normally, `__eq__` should return `NotImplemented` rather than `False`, unless it is absolutely certain that values are different. –  Nov 30 '17 at 16:07
  • See also: https://stackoverflow.com/questions/14246983/compare-assert-equality-of-two-complex-data-structures-containing-numpy-arrays – Craig Nov 30 '17 at 16:14

3 Answers3

10

An __req__ may have been considered more confusing than useful in the language. Consider if class Left defines __eq__ and class Right defines __req__, then Python is obliged to make a consistent decision about who gets called first in Left() == Right() (and we would presumably like the result to be equivalent, either way). They can't both win.

However, the Python datamodel does allow a way to do what you want here. The comparison can be controlled from either side of the operation, but you'll need to define AnyOf in a particular way. To control the eq from the right hand side of a comparison when the left side is an np.ndarray instance, AnyOf should be a subclass of np.ndarray.

if I were to do AnyOf(2,3) == arr then my AnyOf class's __eq__ method never gets called

Actually, no, there's a fundamental misunderstanding evident here. The left side always gets first try at handling the equality comparison, unless the right side type is a subclass of the left side type.

arr == AnyOf(2,3)

In the comparison shown above, your custom __eq__ is being called, because the numpy array calls it! So np.ndarray wins, and it decides to check once per element. It literally could do anything else, including not calling your AnyOf.__eq__ at all.

AnyOf(2,3) == arr

In the comparison shown above, your class does get the first try at the comparison, and it fails because of the way in was used - return other in self.elements is checking if an array is in a tuple.

wim
  • 338,267
  • 99
  • 616
  • 750
  • 4
    Regarding "An `__req__` is not a good idea in the language" and the argument that follows, I don't think that holds, as from the same argument, methods like `__radd__` would also not be a good idea. However, I do see now that, due to how which model is selected (`Left.__eq__` or `Right.__req__`) it would actually make it so that `__req__` would fulfill the exact same function as `__eq__`, making the right-hand method superfluous! – acdr Dec 01 '17 at 08:15
4

This is the documentation on the data model:

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.

As stated in the comments above, what you want works, and __eq__ is essentially the sames as a potential __req__: it is called on the right hand side of == if the object on the left hand side returns NotImplemented:

In [1]: class A:
   ...:     def __eq__(self, other):
   ...:         return NotImplemented
   ...:     

In [2]: class B:
   ...:     def __eq__(self, other): 
   ...:         print("B comparing")
   ...:         return True
   ...:     

In [3]: B() == A()
B comparing
Out[3]: True

In [4]: A() == B()
B comparing
Out[4]: True

In [5]: A() == A()
Out[5]: False

As it comes, it even work with other, ordinary, objects:

In [10]: 5 == B()
B comparing
Out[10]: True

However, some objects may yield a TypeError on __eq__ instead of returning NotImplemented or False, and that makes this not reliable for all kinds of objects.

What happens in your case, is an incorrect use of the operator in with arrays and tuples inside your own __eq__ method. (Thanks @wim to have spotted this in another answer here).

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 1
    +1 that's a big "if" though - some standard types will just raise exception instead of returning `NotImplemented`, so you can't rely on this. – wim Nov 30 '17 at 16:17
  • I'm just not sure it works with `np.array` that way. But if it doesn't it's a bug in `np.array`, not something missing from the language. –  Nov 30 '17 at 16:18
  • I did not say one can rely on it. – jsbueno Nov 30 '17 at 16:19
3

The documentation about the __rxx__ methods like __radd__ states:

These functions are only called if the left operand does not support the corresponding operation and the operands are of different types.

While classes don't have __add__ or __sub__ methods per default, they do have __eq__:

>>> class A(object):
...     pass
>>> '__eq__' in dir(A)
True

This means __req__ would never be called unless you explicitly remove __eq__ from the other class or make __eq__ return NotImplemented.

You can solve your specific problem with np.in1d:

>>> np.in1d(arr, [2, 3])
array([False,  True,  True, False, False], dtype=bool)
Mike Müller
  • 82,630
  • 20
  • 166
  • 161
  • *This means `__req__` would never be called unless you explicitly remove `__eq__` from the other class* <-- that is not correct, it only needs to opt-out by returning `NotImplemented`, and indeed that's actually the default behavior which is inherited from `object.__eq__` - it implements equality by identity ("is"), and returns `NotImplemented` in the case of a non-identical comparison. – wim Aug 27 '21 at 15:57
  • Thanks for pointing this out. Corrected my answer. – Mike Müller Aug 29 '21 at 04:45