在扩展 numbers.Real 的对象上使用 numpy 函数

Use numpy functions on objects extending numbers.Real

PEP 3141 为不同类型的数字引入抽象基 classes 以允许自定义实现。 我想从 numbers.Real 导出一个 class 并计算它的正弦值。使用 pythons math-module,这工作正常。当我在 numpy 中尝试相同的操作时,出现错误。

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))

结果

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

似乎 numpy 试图调用其参数的 sin 方法。对我来说,这很令人困惑,因为标准数据类型(如 float)也没有这样的方法,但 np.sin 可以处理它们。

是否只有某种针对标准数据类型的硬编码检查,并且不支持 PEP 3141?还是我在 class 中遗漏了什么?

因为实现所有必需的方法非常乏味,这里是我当前使用 math-模块的代码:

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))

我刚刚回答了类似的问题,但我会再重复一遍

np.sin(a)

实际上是

np.sin(np.array(a))

np.array(a) 生产什么?它是什么dtype

如果它是对象 dtype 数组,则可以解释该错误。使用对象 dtype 数组,numpy 遍历(引用),并尝试 运行 对每个对象使用适当的方法。对于可以使用 __add__ 之类方法的运算符,这通常是可以的,但几乎没有人定义 sinexp 方法。

从昨天开始

将数字数据类型数组与对象数据类型进行比较:

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 的回答(和 )以了解为什么这不起作用。

阅读文档后,我选择创建一个 custom numpy array container 并添加我自己的 numpy ufunc 支持。相关方法是

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

我选择只支持我自己的数据类型和 numbers.Number ufunc,这使得实现非常简单。 有关详细信息,请参阅 docs

为了扩展numbers.Real,我们还需要定义各种魔术方法(见PEP 3141)。 通过扩展 np.lib.mixins.NDArrayOperatorsMixin(除了 numbers.Real),我们可以免费获得大部分内容。 其余的需要手动实现。

下面你可以看到我的完整代码,它适用于 math 模块函数以及 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))