Совместимость с NumPy#
Объекты ndarray NumPy предоставляют как высокоуровневый API для операций с данными, структурированными в массивы, так и конкретную реализацию API на основе
страйдовое хранение в оперативной памяти. Хотя этот API мощный и достаточно
универсальный, его конкретная реализация имеет ограничения. По мере роста наборов данных и использования NumPy
в различных новых средах и архитектурах возникают случаи,
когда стратегия хранения с шагом в оперативной памяти неприменима, что привело
к перереализации этого API разными библиотеками для собственных нужд. Это включает
массивы GPU (CuPy), Разреженные массивы (scipy.sparse, PyData/Sparse) и параллельные массивы (Dask массивы), а также различные реализации, подобные NumPy, в фреймворках глубокого обучения, такие как TensorFlow и PyTorch. Аналогично, существует множество проектов, построенных поверх API NumPy для помеченных и индексированных массивов (XArray), автоматическое дифференцирование (JAX), маскированные массивы (numpy.ma),
физические единицы (astropy.units, pint, unyt, среди других, которые добавляют дополнительную
функциональность поверх API NumPy.
Тем не менее, пользователи всё ещё хотят работать с этими массивами, используя знакомый API NumPy, и повторно использовать существующий код с минимальными (в идеале нулевыми) затратами на адаптацию. С этой целью определены различные протоколы для реализаций многомерных массивов с высокоуровневыми API, соответствующими NumPy.
В общих чертах, есть три группы функций, используемых для взаимодействия с NumPy:
Методы преобразования стороннего объекта в ndarray;
Методы отложенного выполнения из функции NumPy в другую библиотеку массивов;
Методы, которые используют функции NumPy и возвращают экземпляр стороннего объекта.
Мы описываем эти функции ниже.
1. Использование произвольных объектов в NumPy#
Первый набор функций взаимодействия из API NumPy позволяет обрабатывать иностранные объекты как массивы NumPy, когда это возможно. Когда функции NumPy встречают иностранный объект, они попытаются (в порядке):
Протокол буфера, описанный в документации Python C-API.
The
__array_interface__протокол, описанный на этой странице. Предшественник буферного протокола Python, он определяет способ доступа к содержимому массива NumPy из других расширений на C.The
__array__()метод, который запрашивает у произвольного объекта преобразование себя в массив.
Для обоих буфера и __array_interface__ протоколы, объект описывает свою структуру памяти, и NumPy делает все остальное (нулевое копирование, если возможно). Если это невозможно, сам объект отвечает за возврат ndarray из __array__().
DLPack еще один протокол для преобразования внешних объектов в массивы NumPy независимо от языка и устройства. NumPy неявно не преобразует объекты в ndarrays с помощью DLPack. Он предоставляет функцию
numpy.from_dlpack который принимает любой объект, реализующий __dlpack__ метод
и выводит массив NumPy ndarray (который обычно является представлением буфера данных
входного объекта). Спецификация Python для DLPack страница объясняет __dlpack__ протокол
подробно.
dtype совместимость#
Аналогично __array__() для объектов массива, определяя __numpy_dtype__
позволяет пользовательскому объекту dtype быть совместимым с NumPy. __numpy_dtype__ должен возвращать экземпляр dtype NumPy (обратите внимание, что
np.float64 не является экземпляром dtype, np.dtype(np.float64) является).
Новое в версии 2.4: До NumPy 2.4 .dtype атрибут обрабатывался аналогично. Начиная с NumPy 2.4, оба принимаются и реализуются __numpy_dtype__ предотвращает .dtype
от проверки.
Протокол интерфейса массива#
The протокол интерфейса массива определяет способ для объектов, подобных массивам, повторно использовать буферы данных друг друга. Его реализация опирается на наличие следующих атрибутов или методов:
__array_interface__: словарь Python, содержащий форму, тип элементов и, опционально, адрес буфера данных и шаги массиво-подобного объекта;__array__(): метод, возвращающий копию NumPy ndarray или представление объекта, подобного массиву;
The __array_interface__ атрибут можно проверить напрямую:
>>> import numpy as np
>>> x = np.array([1, 2, 5.0, 8])
>>> x.__array_interface__
{'data': (94708397920832, False), 'strides': None, 'descr': [('', '
The __array_interface__ атрибут также может использоваться для манипуляции данными объекта на месте:
>>> class wrapper():
... pass
...
>>> arr = np.array([1, 2, 3, 4])
>>> buf = arr.__array_interface__
>>> buf
{'data': (140497590272032, False), 'strides': None, 'descr': [('', '
>>> buf['shape'] = (2, 2)
>>> w = wrapper()
>>> w.__array_interface__ = buf
>>> new_arr = np.array(w, copy=False)
>>> new_arr
array([[1, 2],
[3, 4]])
Мы можем проверить, что arr и new_arr используют один и тот же буфер данных:
>>> new_arr[0, 0] = 1000
>>> new_arr
array([[1000, 2],
[ 3, 4]])
>>> arr
array([1000, 2, 3, 4])
The __array__() метод#
The __array__() метод гарантирует, что любой объект, подобный NumPy (массив, любой
объект, предоставляющий интерфейс массива, объект, чей __array__() метод
возвращает массив или любую вложенную последовательность), который его реализует, может использоваться как
массив NumPy. По возможности это будет означать использование __array__() для создания представления NumPy ndarray из объектоподобного массива. В противном случае данные копируются в новый объект ndarray. Это не оптимально, так как приведение массивов к ndarray может вызвать проблемы с производительностью или необходимость копирования и потери метаданных, поскольку исходный объект и любые атрибуты/поведение, которые он мог иметь, теряются.
Сигнатура метода должна быть __array__(self, dtype=None, copy=None).
Если передан dtype не является None и отличается от типа данных объекта, должно произойти приведение к указанному типу. Если copy является None, копия
должна быть создана только если dtype аргумент принудительно применяет это. Для copy=True, копия всегда должна создаваться, где copy=False должен вызывать исключение, если требуется копия.
Если класс реализует старую сигнатуру __array__(self), для np.array(a)
будет выдано предупреждение о том, что dtype и copy аргументы отсутствуют.
Чтобы увидеть пример реализации пользовательского массива, включая использование
__array__(), см. Создание пользовательских контейнеров массивов.
Протокол DLPack#
The DLPack протокол определяет структуру памяти страйдовых n-мерных объектов массива. Он предлагает следующий синтаксис для обмена данными:
A
numpy.from_dlpackфункция, которая принимает (массив) объекты с__dlpack__метод и использует этот метод для создания нового массива, содержащего данные изx.__dlpack__(self, stream=None)и__dlpack_device__методы на объекте массива, которые будут вызываться изнутриfrom_dlpack, чтобы запросить, на каком устройстве находится массив (может потребоваться для передачи правильного потока, например, в случае нескольких GPU) и для доступа к данным.
В отличие от буферного протокола, DLPack позволяет обмениваться массивами, содержащими данные на устройствах, отличных от CPU (например, Vulkan или GPU). Поскольку NumPy поддерживает только CPU, он может преобразовывать только объекты, данные которых находятся на CPU. Но другие библиотеки, такие как PyTorch и CuPy, могут обмениваться данными на GPU с использованием этого протокола.
2. Работа с внешними объектами без преобразования#
Второй набор методов, определенных API NumPy, позволяет нам отложить выполнение функции NumPy в другую библиотеку массивов.
Рассмотрим следующую функцию.
>>> import numpy as np
>>> def f(x):
... return np.mean(np.exp(x))
Обратите внимание, что np.exp является универсальная функция (ufunc), что означает, что он работает с ndarrays поэлементно. С другой стороны, np.mean работает вдоль одной из осей массива.
Мы можем применить f в объект NumPy ndarray напрямую:
>>> x = np.array([1, 2, 3, 4])
>>> f(x)
21.1977562209304
Мы хотим, чтобы эта функция одинаково хорошо работала с любым объектом массива, подобным NumPy.
NumPy позволяет классу указать, что он хотел бы обрабатывать вычисления пользовательским способом через следующие интерфейсы:
__array_ufunc__: позволяет сторонним объектам поддерживать и переопределять универсальные функции (ufuncs).__array_function__: общий раздел для функциональности NumPy, которая не охвачена__array_ufunc__протокол для универсальных функций.
Пока внешние объекты реализуют __array_ufunc__ или
__array_function__ протоколами, можно работать с ними без необходимости явного преобразования.
The __array_ufunc__ protocol#
A универсальная функция (или сокращённо ufunc) является
«векторизованной» оболочкой для функции, которая принимает фиксированное количество конкретных входных данных
и производит фиксированное количество конкретных выходных данных. Выходные данные ufunc (и
его методов) не обязательно являются ndarray, если не все входные аргументы
являются ndarray. Действительно, если любой вход определяет __array_ufunc__ метод, управление
будет полностью передано этой функции, т.е. ufunc переопределён. The
__array_ufunc__ метод, определённый для этого (не ndarray) объекта, имеет доступ к
универсальной функции NumPy. Поскольку универсальные функции имеют чёткую структуру, внешний
__array_ufunc__ метод может полагаться на атрибуты ufunc, такие как .at(),
.reduce()и другие.
Подкласс может переопределить поведение при выполнении NumPy ufuncs на нем,
переопределив значение по умолчанию ndarray.__array_ufunc__ метод. Этот метод выполняется вместо ufunc и должен возвращать либо результат операции, либо NotImplemented если запрошенная операция не реализована.
The __array_function__ protocol#
Для достижения достаточного покрытия API NumPy для поддержки зависимых проектов
необходимо выйти за рамки __array_ufunc__ и реализовать протокол, который
позволяет аргументам функции NumPy взять контроль и перенаправить выполнение в
другую функцию (например, реализацию на GPU или параллельную) таким образом,
который безопасен и согласован между проектами.
Семантика __array_function__ очень похожи на __array_ufunc__, за исключением того, что операция задается произвольным вызываемым объектом, а не экземпляром ufunc и методом. Для получения дополнительных сведений см. NEP 18 — Механизм диспетчеризации для высокоуровневых функций массивов NumPy.
3. Возврат внешних объектов#
Третий тип набора функций предназначен для использования реализации функции NumPy и последующего преобразования возвращаемого значения обратно в экземпляр стороннего объекта. __array_finalize__ и __array_wrap__ методы работают за кулисами, чтобы гарантировать, что тип возвращаемого значения функции NumPy может быть указан по необходимости.
The __array_finalize__ метод — это механизм, который NumPy предоставляет для того, чтобы подклассы могли обрабатывать различные способы создания новых экземпляров. Этот метод вызывается всякий раз, когда система внутренне выделяет новый массив из объекта, который является подклассом (подтипом) ndarray. Он может использоваться для изменения атрибутов после создания или для обновления мета-информации от «родителя».
The __array_wrap__ метод «завершает действие» в смысле разрешения любому
объекту (например, пользовательским функциям) устанавливать тип возвращаемого значения и
обновлять атрибуты и метаданные. Это можно рассматривать как противоположность
__array__ метод. В конце каждого объекта, который реализует
__array_wrap__, этот метод вызывается на входном объекте с наивысшим
приоритет массива, или выходной объект, если он был указан.
__array_priority__ атрибут используется для определения типа объекта для возврата в ситуациях, когда существует более одной возможности для типа Python возвращаемого объекта. Например, подклассы могут использовать этот метод для преобразования выходного массива в экземпляр подкласса и обновления метаданных перед возвратом массива пользователю.
Для получения дополнительной информации об этих методах см. Наследование от ndarray и Особенности подтипирования ndarray.
Примеры взаимодействия#
Пример: Pandas Series объекты#
Рассмотрим следующее:
>>> import pandas as pd
>>> ser = pd.Series([1, 2, 3, 4])
>>> type(ser)
pandas.core.series.Series
Теперь, ser является не ndarray, но поскольку он
реализует протокол __array_ufunc__,
мы можем применять к нему универсальные функции, как если бы это был ndarray:
>>> np.exp(ser)
0 2.718282
1 7.389056
2 20.085537
3 54.598150
dtype: float64
>>> np.sin(ser)
0 0.841471
1 0.909297
2 0.141120
3 -0.756802
dtype: float64
Мы даже можем выполнять операции с другими ndarrays:
>>> np.add(ser, np.array([5, 6, 7, 8]))
0 6
1 8
2 10
3 12
dtype: int64
>>> f(ser)
21.1977562209304
>>> result = ser.__array__()
>>> type(result)
numpy.ndarray
Пример: тензоры PyTorch#
PyTorch является оптимизированной библиотекой тензоров для глубокого обучения с использованием GPU и CPU. Массивы PyTorch обычно называются тензорыТензоры похожи на ndarrays из NumPy, за исключением того, что тензоры могут работать на GPU или других аппаратных ускорителях. Фактически, тензоры и массивы NumPy часто могут использовать одну и ту же базовую память, устраняя необходимость копирования данных.
>>> import torch
>>> data = [[1, 2],[3, 4]]
>>> x_np = np.array(data)
>>> x_tensor = torch.tensor(data)
Обратите внимание, что x_np и x_tensor являются разными типами объектов:
>>> x_np
array([[1, 2],
[3, 4]])
>>> x_tensor
tensor([[1, 2],
[3, 4]])
Однако мы можем рассматривать тензоры PyTorch как массивы NumPy без необходимости явного преобразования:
>>> np.exp(x_tensor)
tensor([[ 2.7183, 7.3891],
[20.0855, 54.5982]], dtype=torch.float64)
Также обратите внимание, что тип возвращаемого значения этой функции совместим с исходным типом данных.
Предупреждение
Хотя такое смешение ndarray и тензоров может быть удобным, оно не рекомендуется. Это не будет работать для тензоров не на CPU и приведет к неожиданному поведению в крайних случаях. Пользователям следует предпочесть явное преобразование ndarray в тензор.
Примечание
PyTorch не реализует __array_function__ или __array_ufunc__.
Под капотом, Tensor.__array__() метод возвращает NumPy ndarray как
представление буфера данных тензора. См. этой проблемы и
реализация __torch_function__
подробности.
Также обратите внимание, что мы можем видеть __array_wrap__ в действии здесь, хотя
torch.Tensor не является подклассом ndarray:
>>> import torch
>>> t = torch.arange(4)
>>> np.abs(t)
tensor([0, 1, 2, 3])
PyTorch реализует __array_wrap__ чтобы иметь возможность получать тензоры обратно из функций NumPy,
и мы можем напрямую изменять его, чтобы контролировать, какие типы объектов
возвращаются из этих функций.
Пример: массивы CuPy#
CuPy — это библиотека массивов, совместимая с NumPy/SciPy, для вычислений с ускорением на GPU в Python. CuPy реализует подмножество интерфейса NumPy, реализуя
cupy.ndarray, аналог NumPy ndarrays.
>>> import cupy as cp
>>> x_gpu = cp.array([1, 2, 3, 4])
The cupy.ndarray объект реализует __array_ufunc__ интерфейса. Это позволяет применять универсальные функции NumPy к массивам CuPy (это отложит операцию до соответствующей реализации универсальной функции CUDA/ROCm в CuPy):
>>> np.mean(np.exp(x_gpu))
array(21.19775622)
Обратите внимание, что тип возвращаемого значения этих операций всё ещё согласуется с исходным типом:
>>> arr = cp.random.randn(1, 2, 3, 4).astype(cp.float32)
>>> result = np.sum(arr)
>>> print(type(result))
См. эта страница в документации CuPy для подробностей.
cupy.ndarray также реализует __array_function__ интерфейс, что означает возможность выполнения операций, таких как
>>> a = np.random.randn(100, 100)
>>> a_gpu = cp.asarray(a)
>>> qr_gpu = np.linalg.qr(a_gpu)
CuPy реализует многие функции NumPy на cupy.ndarray объекты, но не все.
См. документация CuPy
подробности.
Пример: массивы Dask#
Dask — это гибкая библиотека для параллельных вычислений в Python. Dask Array реализует подмножество интерфейса NumPy ndarray, используя блочные алгоритмы, разбивая большой массив на множество маленьких массивов. Это позволяет выполнять вычисления над массивами, превышающими размер оперативной памяти, используя несколько ядер.
Dask поддерживает __array__() и __array_ufunc__.
>>> import dask.array as da
>>> x = da.random.normal(1, 0.1, size=(20, 20), chunks=(10, 10))
>>> np.mean(np.exp(x))
dask.array
>>> np.mean(np.exp(x)).compute()
5.090097550553843
Примечание
Dask вычисляется лениво, и результат вычисления не вычисляется до тех пор, пока вы не запросите его, вызвав compute().
См. документация массива Dask и область взаимодействия массивов Dask с массивами NumPy подробности.
Пример: DLPack#
Несколько библиотек Python для анализа данных реализуют __dlpack__ протокол.
Среди них PyTorch и CuPy. Полный список библиотек, реализующих
этот протокол, можно найти на
эта страница документации DLPack.
Преобразовать тензор PyTorch CPU в массив NumPy:
>>> import torch
>>> x_torch = torch.arange(5)
>>> x_torch
tensor([0, 1, 2, 3, 4])
>>> x_np = np.from_dlpack(x_torch)
>>> x_np
array([0, 1, 2, 3, 4])
>>> # note that x_np is a view of x_torch
>>> x_torch[1] = 100
>>> x_torch
tensor([ 0, 100, 2, 3, 4])
>>> x_np
array([ 0, 100, 2, 3, 4])
Импортированные массивы доступны только для чтения, поэтому запись или операции на месте завершатся ошибкой:
>>> x.flags.writeable
False
>>> x_np[1] = 1
Traceback (most recent call last):
File "" , line 1, in
ValueError: assignment destination is read-only
Для работы с импортированными массивами на месте необходимо создать копию, но это приведёт к дублированию памяти. Не делайте этого для очень больших массивов:
>>> x_np_copy = x_np.copy()
>>> x_np_copy.sort() # works
Примечание
Обратите внимание, что тензоры GPU нельзя преобразовать в массивы NumPy, так как NumPy не поддерживает устройства GPU:
>>> x_torch = torch.arange(5, device='cuda')
>>> np.from_dlpack(x_torch)
Traceback (most recent call last):
File "" , line 1, in
RuntimeError: Unsupported device in DLTensor.
Но если обе библиотеки поддерживают устройство, на котором находится буфер данных, можно
использовать __dlpack__ протокол (например, PyTorch и CuPy):
>>> x_torch = torch.arange(5, device='cuda')
>>> x_cupy = cupy.from_dlpack(x_torch)
Аналогично, массив NumPy можно преобразовать в тензор PyTorch:
>>> x_np = np.arange(5)
>>> x_torch = torch.from_dlpack(x_np)
Массивы только для чтения не могут быть экспортированы:
>>> x_np = np.arange(5)
>>> x_np.flags.writeable = False
>>> torch.from_dlpack(x_np)
Traceback (most recent call last):
File "" , line 1, in
File ".../site-packages/torch/utils/dlpack.py", line 63, in from_dlpack
dlpack = ext_tensor.__dlpack__()
TypeError: NumPy currently only supports dlpack for writeable arrays
Дополнительное чтение#
Специальные атрибуты и методы (подробности о
__array_ufunc__и__array_function__протоколы)Наследование от ndarray (подробности о
__array_wrap__и__array_finalize__методы)Особенности подтипирования ndarray (подробнее о реализации
__array_finalize__,__array_wrap__и__array_priority__)