Subclassing Numpy Array - атрибуты распространения

Я хотел бы знать, как можно распространять пользовательские атрибуты массивов numpy, даже когда массив проходит через такие функции, как np.fromfunction.

Например, мой класс ExampleTensor определяет атрибут attr который по умолчанию установлен в 1.

import numpy as np

class ExampleTensor(np.ndarray):
    def __new__(cls, input_array):
        return np.asarray(input_array).view(cls)

    def __array_finalize__(self, obj) -> None:
        if obj is None: return
        # This attribute should be maintained!
        self.attr = getattr(obj, 'attr', 1)

Нарезка и основные операции между экземплярами ExampleTensor будут поддерживать атрибуты, но использование других функций numpy не будет (вероятно, потому, что они создают обычные массивы numpy вместо ExampleTensors). Мой вопрос: существует ли решение, которое сохраняется в пользовательских атрибутах, когда обычный массив numpy создается из экземпляров массивов с подклассами numpy?

Пример для воспроизведения проблемы:

ex1 = ExampleTensor([[3, 4],[5, 6]])
ex1.attr = "some val"

print(ex1[0].attr)    # correctly outputs "some val"
print((ex1+ex1).attr) # correctly outputs "some val"

np.sum([ex1, ex1], axis=0).attr # Attribute Error: 'numpy.ndarray' object has no attribute 'attr'

Ответ 1

import numpy as np

class ExampleTensor(np.ndarray):
    def __new__(cls, input_array):
        return np.asarray(input_array).view(cls)

    def __array_finalize__(self, obj) -> None:
        if obj is None: return
        # This attribute should be maintained!
        default_attributes = {"attr": 1}
        self.__dict__.update(default_attributes)  # another way to set attributes

Реализовать метод array_ufunc, подобный этому

    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):  # this method is called whenever you use a ufunc
        f = {
            "reduce": ufunc.reduce,
            "accumulate": ufunc.accumulate,
            "reduceat": ufunc.reduceat,
            "outer": ufunc.outer,
            "at": ufunc.at,
            "__call__": ufunc,
        }
        output = ExampleTensor(f[method](*(i.view(np.ndarray) for i in inputs), **kwargs))  # convert the inputs to np.ndarray to prevent recursion, call the function, then cast it back as ExampleTensor
        output.__dict__ = self.__dict__  # carry forward attributes
        return output

Тестовое задание

x = ExampleTensor(np.array([1,2,3]))
x.attr = 2

y0 = np.add(x, x)
print(y0, y0.attr)
y1 = np.add.outer(x, x)
print(y1, y1.attr)  # works even if called with method

[2 4 6] 2
[[2 3 4]
 [3 4 5]
 [4 5 6]] 2

Объяснение в комментариях.

Ответ 2

Я думаю, что ваш пример неверен:

>>> type(ex1)
<class '__main__.ExampleTensor'>

но

>>> type([ex1, ex1])
<class 'numpy.ndarray'>

для которых ваши перегруженные __new__ и __array_finalize__ не вызываются, поскольку вы на самом деле создаете массив, а не ваш подкласс. Однако они вызываются, если вы делаете:

>>> ExampleTensor([ex1, ex1])

который устанавливает attr = 1 поскольку вы не определили, как распространять атрибут при построении ExampleTensor из списка ExampleTensor. Вам нужно будет определить это поведение в вашем подклассе, перегрузив соответствующие операции. Как было предложено в комментариях выше, стоит взглянуть на код для np.matrix для вдохновения.

Ответ 3

Какое значение должно "распространяться", если ex1.attr != ex2.attr для np.sum([ex1, ex2], axis=0).attr?

Обратите внимание, что этот вопрос является более фундаментальным, чем может показаться первым: как вообще большое количество функций numpy могло бы самостоятельно узнать ваше намерение? Вероятно, вам не удастся написать перегруженную версию для каждой из функций "attr-aware", таких как:

def sum(a, **kwargs):
    sa=np.sum(a, **kwargs)
    if isinstance(a[0],ExampleTensor): # or if hasattr(a[0],'attr')
        sa.attr=a[0].attr
    return sa

Я уверен, что этого недостаточно для обработки любого входа np.sum(), но он должен работать для вашего примера.