Создание пользовательских контейнеров массивов
Механизм диспетчеризации NumPy, представленный в версии numpy v1.16, является рекомендуемым подходом для создания пользовательских контейнеров N-мерных массивов, совместимых с API NumPy и предоставляющих пользовательские реализации функций NumPy. Примеры применения включают массивы dask — N-мерный массив, распределенный по нескольким узлам, и массивы cupy — N-мерный массив на графическом процессоре.
Для понимания принципов создания пользовательских контейнеров массивов начнём с простого примера, имеющего ограниченное применение, но демонстрирующего основные концепции.
>>> import numpy as np
>>> class DiagonalArray:
... def __init__(self, N, value):
... self._N = N
... self._i = value
... def __repr__(self):
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
... def __array__(self, dtype=None, copy=None):
... if copy is False:
... raise ValueError(
... "`copy=False` isn't supported. A copy is always created."
... )
... return self._i * np.eye(self._N, dtype=dtype)
Наш пользовательский массив можно создать следующим образом:
>>> arr = DiagonalArray(5, 1) >>> arr DiagonalArray(N=5, value=1)
Его можно преобразовать в массив NumPy, используя numpy.array или numpy.asarray, которые вызовут его метод __array__ для получения стандартного numpy.ndarray.
>>> np.asarray(arr)
array([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
Если мы применим функцию NumPy к arr, NumPy снова воспользуется интерфейсом __array__ для преобразования его в массив и затем применит функцию стандартным способом.
>>> np.multiply(arr, 2)
array([[2., 0., 0., 0., 0.],
[0., 2., 0., 0., 0.],
[0., 0., 2., 0., 0.],
[0., 0., 0., 2., 0.],
[0., 0., 0., 0., 2.]])
Обратите внимание, что возвращаемый тип — это стандартный numpy.ndarray.
>>> type(np.multiply(arr, 2)) <class 'numpy.ndarray'>
Как передать наш пользовательский тип массива через эту функцию? NumPy позволяет классу указывать, что он хочет обрабатывать вычисления пользовательским способом через интерфейсы __array_ufunc__ и __array_function__. Рассмотрим их по очереди, начиная с __array_ufunc__. Этот метод охватывает Универсальные функции (ufunc), класс функций, который включает, например, numpy.multiply и numpy.sin.
Метод __array_ufunc__ получает:
-
ufunc, функцию, например,numpy.multiply -
method, строку, различающуюnumpy.multiply(...)и варианты, такие какnumpy.multiply.outer,numpy.multiply.accumulate, и так далее. В общем случае этоnumpy.multiply(...),method == '__call__'. -
inputs, который может быть смесью различных типов -
kwargs, ключевые аргументы, передаваемые функции
В этом примере мы будем обрабатывать только метод __call__
>>> from numbers import Number
>>> class DiagonalArray:
... def __init__(self, N, value):
... self._N = N
... self._i = value
... def __repr__(self):
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
... def __array__(self, dtype=None, copy=None):
... if copy is False:
... raise ValueError(
... "`copy=False` isn't supported. A copy is always created."
... )
... return self._i * np.eye(self._N, dtype=dtype)
... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
... if method == '__call__':
... N = None
... scalars = []
... for input in inputs:
... if isinstance(input, Number):
... scalars.append(input)
... elif isinstance(input, self.__class__):
... scalars.append(input._i)
... if N is not None:
... if N != input._N:
... raise TypeError("inconsistent sizes")
... else:
... N = input._N
... else:
... return NotImplemented
... return self.__class__(N, ufunc(*scalars, **kwargs))
... else:
... return NotImplemented
Теперь наш пользовательский тип массива проходит через функции NumPy.
>>> arr = DiagonalArray(5, 1) >>> np.multiply(arr, 3) DiagonalArray(N=5, value=3) >>> np.add(arr, 3) DiagonalArray(N=5, value=4) >>> np.sin(arr) DiagonalArray(N=5, value=0.8414709848078965)
На данном этапе arr + 3 не работает.
>>> arr + 3 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +: 'DiagonalArray' and 'int'
Для его поддержки необходимо определить Python-интерфейсы __add__, __lt__, и так далее, чтобы передать управление соответствующему ufunc. Это удобно сделать, унаследовав от миксина NDArrayOperatorsMixin.
>>> import numpy.lib.mixins
>>> class DiagonalArray(numpy.lib.mixins.NDArrayOperatorsMixin):
... def __init__(self, N, value):
... self._N = N
... self._i = value
... def __repr__(self):
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
... def __array__(self, dtype=None, copy=None):
... if copy is False:
... raise ValueError(
... "`copy=False` isn't supported. A copy is always created."
... )
... return self._i * np.eye(self._N, dtype=dtype)
... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
... if method == '__call__':
... N = None
... scalars = []
... for input in inputs:
... if isinstance(input, Number):
... scalars.append(input)
... elif isinstance(input, self.__class__):
... scalars.append(input._i)
... if N is not None:
... if N != input._N:
... raise TypeError("inconsistent sizes")
... else:
... N = input._N
... else:
... return NotImplemented
... return self.__class__(N, ufunc(*scalars, **kwargs))
... else:
... return NotImplemented
>>> arr = DiagonalArray(5, 1) >>> arr + 3 DiagonalArray(N=5, value=4) >>> arr > 0 DiagonalArray(N=5, value=True)
Теперь перейдём к __array_function__. Создадим словарь, сопоставляющий функции NumPy с нашими пользовательскими вариантами.
>>> HANDLED_FUNCTIONS = {}
>>> class DiagonalArray(numpy.lib.mixins.NDArrayOperatorsMixin):
... def __init__(self, N, value):
... self._N = N
... self._i = value
... def __repr__(self):
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
... def __array__(self, dtype=None, copy=None):
... if copy is False:
... raise ValueError(
... "`copy=False` isn't supported. A copy is always created."
... )
... return self._i * np.eye(self._N, dtype=dtype)
... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
... if method == '__call__':
... N = None
... scalars = []
... for input in inputs:
... # In this case we accept only scalar numbers or DiagonalArrays.
... if isinstance(input, Number):
... scalars.append(input)
... elif isinstance(input, self.__class__):
... scalars.append(input._i)
... if N is not None:
... if N != input._N:
... raise TypeError("inconsistent sizes")
... else:
... N = input._N
... else:
... return NotImplemented
... return self.__class__(N, ufunc(*scalars, **kwargs))
... else:
... return NotImplemented
... def __array_function__(self, func, types, args, kwargs):
... if func not in HANDLED_FUNCTIONS:
... return NotImplemented
... # Note: this allows subclasses that don't override
... # __array_function__ to handle DiagonalArray objects.
... if not all(issubclass(t, self.__class__) for t in types):
... return NotImplemented
... return HANDLED_FUNCTIONS[func](*args, **kwargs)
...
Удобным подходом является определение декоратора implements, который можно использовать для добавления функций в HANDLED_FUNCTIONS.
>>> def implements(np_function): ... "Register an __array_function__ implementation for DiagonalArray objects." ... def decorator(func): ... HANDLED_FUNCTIONS[np_function] = func ... return func ... return decorator ...
Теперь напишем реализации функций NumPy для DiagonalArray. Для полноты, чтобы поддерживать использование arr.sum(), добавим метод sum, который вызывает numpy.sum(self), и аналогично для mean.
>>> @implements(np.sum) ... def sum(arr): ... "Implementation of np.sum for DiagonalArray objects" ... return arr._i * arr._N ... >>> @implements(np.mean) ... def mean(arr): ... "Implementation of np.mean for DiagonalArray objects" ... return arr._i / arr._N ... >>> arr = DiagonalArray(5, 1) >>> np.sum(arr) 5 >>> np.mean(arr) 0.2
Если пользователь попытается использовать какие-либо функции NumPy, не включённые в HANDLED_FUNCTIONS, NumPy поднимет исключение TypeError, указывая, что данная операция не поддерживается. Например, конкатенация двух DiagonalArrays не даёт другой диагональный массив, поэтому она не поддерживается.
>>> np.concatenate([arr, arr]) Traceback (most recent call last): ... TypeError: no implementation found for 'numpy.concatenate' on types that implement __array_function__: [<class '__main__.DiagonalArray'>]
Кроме того, наши реализации sum и mean не принимают необязательные аргументы, которые принимает реализация NumPy.
>>> np.sum(arr, axis=0) Traceback (most recent call last): ... TypeError: sum() got an unexpected keyword argument 'axis'
Пользователь всегда может преобразовать в обычный numpy.ndarray с помощью numpy.asarray и использовать стандартные функции NumPy.
>>> np.concatenate([np.asarray(arr), np.asarray(arr)])
array([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.],
[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
Реализация DiagonalArray в этом примере обрабатывает только функции np.sum и np.mean для краткости. Многие другие функции API NumPy также можно обернуть, и полноценный пользовательский контейнер массивов может явно поддерживать все функции, предоставляемые NumPy для обертывания.
NumPy предоставляет некоторые утилиты для тестирования пользовательских контейнеров массивов, реализующих протоколы __array_ufunc__ и __array_function__ в пространстве имён numpy.testing.overrides.
Чтобы проверить, можно ли переопределить функцию NumPy с помощью __array_ufunc__, можно использовать allows_array_ufunc_override:
>>> from np.testing.overrides import allows_array_ufunc_override >>> allows_array_ufunc_override(np.add) True
Аналогично, можно проверить, можно ли переопределить функцию с помощью __array_function__ с помощью allows_array_function_override.
Списки всех переопределяемых функций в API NumPy также доступны через get_overridable_numpy_array_functions для функций, поддерживающих протокол __array_function__, и get_overridable_numpy_ufuncs для функций, поддерживающих протокол __array_ufunc__. Обе функции возвращают наборы функций, присутствующих в публичном API NumPy. Пользовательские ufunc или ufunc, определённые в других библиотеках, зависящих от NumPy, в этих наборах отсутствуют.
Для получения более подробных примеров пользовательских контейнеров массивов обратитесь к исходному коду dask и cupy.
См. также NEP 18.
© 2005–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/user/basics.dispatch.html