3

Consider the following snippet:

import numpy as np

class MyClass:
    def __rsub__(self, other):
        print(type(other), other)

obj = MyClass()
arr = np.ones(3)

arr - obj

I would expect it to print <class 'numpy.ndarray'> [1. 1. 1.]. But in fact, it seems to call to __rsub__ for every element:

<class 'float'> 1.0
<class 'float'> 1.0
<class 'float'> 1.0

Is there any way to tell numpy I want the first behavior, i.e. delegating the entire substraction to MyClass?

lum
  • 1,503
  • 12
  • 17

1 Answers1

3

On NumPy 1.13+, you can do this with NumPy-specific hooks, but you can't have your __rsub__ beat __sub__ methods in general.


__rsub__ is tried if the left operand's __sub__ can't handle the operation, but the left operand can handle this operation. A NumPy array's __sub__ will accept any RHS and perform a broadcasted subtraction. Your object's __rsub__ only comes into play for the individual operations in the broadcasted subtraction.

There is one very limited case where __rsub__ is tried first, which is if the class of the RHS is a subclass of the class of the LHS. You could technically subclass numpy.ndarray, but that would come with a lot of extra baggage and still do nothing for numpy.matrix([[1]]) - obj or other subclasses.

There is no way to say "I want my __rsub__ to win over everything". It doesn't exist, and it wouldn't make sense for it to exist, because what would happen if you tried to subtract two objects that both want to declare their method beats everything?


So that's the general case. Specifically for NumPy, though, you can hook into the mechanisms that numpy.ndarray.__sub__ delegates to.

NumPy arrays delegate __sub__ to the NumPy ufunc machinery. There are a bunch of weird customization options there, but we're interested in a specific use of one specific option: by setting __array_ufunc__ to None at class level, you can declare a class incompatible with ufuncs. This means that all NumPy operator overloads will return NotImplemented, letting your class handle the operation. This affects all operators and a bunch more stuff, but in a way that you probably want:

class MyClass:
    __array_ufunc__ = None
    def __rsub__(self, other):
        print(type(other), other)

If you want something more targeted than blocking all ufuncs, you can implement an actual __array_ufunc__ method and just handle the case where the ufunc is subtraction and an instance of your class is the RHS:

class MyClass:
    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
        if ufunc is not numpy.subtract:
            return NotImplemented
        if method != '__call__':
            return NotImplemented
        if len(inputs) != 2 or inputs[1] is not self:
            return NotImplemented
        if kwargs:
            return NotImplemented
        return self.__rsub__(inputs[0])
    def __rsub__(self, other):
        print(type(other), other)
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Thanks for the explanation. Makes sense. – lum Oct 16 '19 at 08:47
  • 1
    @lum: See expanded answer for a NumPy-specific workaround. You can't make your `__rsub__` beat `__sub__` methods in general, but you *can* tell the NumPy ufunc machinery not to handle instances of your class. – user2357112 Oct 16 '19 at 08:54
  • ah, that's really interesting! Thanks a lot for the pointer, I believe that's exactly what I was looking for. – lum Oct 16 '19 at 09:08
  • how about `__array_priority__`? https://stackoverflow.com/q/40252765/901925v – hpaulj Oct 16 '19 at 09:43
  • @hpaulj: That's part of an old system the devs want to remove, so I didn't want to suggest it. Also, there are issues with knowing what's a high enough value. – user2357112 Oct 16 '19 at 09:53