2

PEP 3141 introduces abstract base classes for different kinds of numbers to allow custom implementations. I want to derive a class from numbers.Real and calculate its sine value. Using pythons math-module, this works fine. When I try the same in numpy, I get an error.

from numbers import Real
import numpy as np
import math

class Mynum(Real):
    def __float__(self):
        return 0.0
    # Many other definitions

a = Mynum()

print("math:")
print(math.sin(a))
print("numpy:")
print(np.sin(a))

results in

math:
0.0
numpy:
AttributeError: 'Mynum' object has no attribute 'sin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
[...]
in <module>
    print(np.sin(a)) TypeError: loop of ufunc does not support argument 0 of type Mynum which has no callable sin method

It seems like numpy tries to call a sin-method of its argument. To me, this is quite confusing since the standard data types (like float) do not have such a method either, but np.sin works on them.

Is there just some kind of hardcoded check for standard data types, and PEP 3141 is not supported? Or did I miss something in my class?

Because it is quite tedious to implement all required methods, here is my current code that works with the math-module:

from numbers import Real
import numpy as np
import math

class Mynum(Real):
    def __init__(self):
        pass

    def __abs__(self):
        pass

    def __add__(self):
        pass

    def __ceil__(self):
        pass

    def __eq__(self):
        pass

    def __float__(self):
        return 0.0

    def __floor__(self):
        pass

    def __floordiv__(self):
        pass

    def __le__(self):
        pass

    def __lt__(self):
        pass

    def __mod__(self):
        pass

    def __mul__(self):
        pass

    def __neg__(self):
        pass

    def __pos__(self):
        pass

    def __pow__(self):
        pass

    def __radd__(self):
        pass

    def __rfloordiv__(self):
        pass

    def __rmod__(self):
        pass

    def __rmul__(self):
        pass

    def __round__(self):
        pass

    def __rpow__(self):
        pass

    def __rtruediv__(self):
        pass

    def __truediv__(self):
        pass

    def __trunc__(self):
        pass

a = Mynum()
print("math:")
print(math.sin(a))
print("numpy:")
print(np.sin(a))
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
502E532E
  • 431
  • 2
  • 11

2 Answers2

3

I just answered something like this, but I'll repeat myself

np.sin(a)

is actually

np.sin(np.array(a))

What does np.array(a) produce? What's its dtype?

If it's an object dtype array, that explains that error. With object dtype array, numpy iterates through (the references), and tries to run an appropriate method on each. That's generally ok with operators which can use __add__ like methods, but almost no one defines a sin or exp method.

From yesterday

How can I make my class more robust to operator/function overloading?

Comparing a numeric dtype array with an object dtype:

In [428]: np.sin(np.array([1,2,3]))
Out[428]: array([0.84147098, 0.90929743, 0.14112001])

In [429]: np.sin(np.array([1,2,3], object))
AttributeError: 'int' object has no attribute 'sin'

The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "<ipython-input-429-d6927b9a87c7>", line 1, in <module>
    np.sin(np.array([1,2,3], object))
TypeError: loop of ufunc does not support argument 0 of type int which has no callable sin method
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • The referenced question was really helpful, especially the following doc entry describing how to create a custom array type: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch. See my own answer below for more detail on what worked for me. – 502E532E Dec 17 '21 at 21:30
0

See hpaulj’s answer (and the linked question) for the explanation why this did not work.

After reading the documentation, I chose to create a custom numpy array container and add my own numpy ufunc support. The relevant method is

def __array_ufunc__(self, ufunc, method, *args, **kwargs):
    if method == "__call__":
        scalars = []
        for arg in args:
            # CAUTION: order matters here because Mynum is also a number
            if isinstance(arg, self.__class__):
                scalars.append(arg.value)
            elif isinstance(arg, Number):
                scalars.append(arg)
            else:
                return NotImplemented
        return self.__class__(ufunc(*scalars, **kwargs))
    return NotImplemented

I chose to support only my own datatype and numbers.Number for the ufuncs, which made the implementation quite simple. See the docs for more detail.

In order to extend numbers.Real, we also need to define various magic methods (see PEP 3141). By extending np.lib.mixins.NDArrayOperatorsMixin (in addition to numbers.Real), we get most of those for free. The remaining ones need to be implemented manually.

Below you can see my complete code, which works for math-module functions as well as numpys.

from numbers import Real, Number
import numpy as np
import math


class Mynum(np.lib.mixins.NDArrayOperatorsMixin, Real):
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return f"{self.__class__.__name__}(value={self.value})"

    def __array__(self, dtype=None):
        return np.array(self.value, dtype=dtype)

    def __array_ufunc__(self, ufunc, method, *args, **kwargs):
        if method == "__call__":
            scalars = []
            for arg in args:
                # CAUTION: order matters here because Mynum is also a number
                if isinstance(arg, self.__class__):
                    scalars.append(arg.value)
                elif isinstance(arg, Number):
                    scalars.append(arg)
                else:
                    return NotImplemented
            return self.__class__(ufunc(*scalars, **kwargs))
        return NotImplemented

    # Below methods are needed because we are extending numbers.Real
    # NDArrayOperatorsMixin takes care of the remaining magic functions

    def __float__(self, *args, **kwargs):
        return self.value.__float__(*args, **kwargs)

    def __ceil__(self, *args, **kwargs):
        return self.value.__ceil__(*args, **kwargs)

    def __floor__(self, *args, **kwargs):
        return self.value.__floor__(*args, **kwargs)

    def __round__(self, *args, **kwargs):
        return self.value.__round__(*args, **kwargs)

    def __trunc__(self, *args, **kwargs):
        return self.value.__trunc__(*args, **kwargs)


a = Mynum(0)

print("math:")
print(math.sin(a))
print("numpy:")
print(np.sin(a))
502E532E
  • 431
  • 2
  • 11