Interoperability with NumPy#
NumPy’s ndarray objects provide both a high-level API for operations onarray-structured data and a concrete implementation of the API based onstrided in-RAM storage. While this API is powerful and fairlygeneral, its concrete implementation has limitations. As datasets grow and NumPybecomes used in a variety of new environments and architectures, there are caseswhere the strided in-RAM storage strategy is inappropriate, which has causeddifferent libraries to reimplement this API for their own uses. This includesGPU arrays (CuPy), Sparse arrays (scipy.sparse,PyData/Sparse)and parallel arrays (Dask arrays) as well as various NumPy-like implementationsin deep learning frameworks, likeTensorFlow andPyTorch. Similarly, there aremany projects that build on top of the NumPy API for labeled and indexed arrays(XArray), automatic differentiation (JAX), masked arrays (numpy.ma),physical units (astropy.units,pint,unyt), among others that add additionalfunctionality on top of the NumPy API.
Yet, users still want to work with these arrays using the familiar NumPy API andreuse existing code with minimal (ideally zero) porting overhead. With thisgoal in mind, various protocols are defined for implementations ofmulti-dimensional arrays with high-level APIs matching NumPy.
Broadly speaking, there are three groups of features used for interoperabilitywith NumPy:
Methods of turning a foreign object into an ndarray;
Methods of deferring execution from a NumPy function to another arraylibrary;
Methods that use NumPy functions and return an instance of a foreign object.
We describe these features below.
1. Using arbitrary objects in NumPy#
The first set of interoperability features from the NumPy API allows foreignobjects to be treated as NumPy arrays whenever possible. When NumPy functionsencounter a foreign object, they will try (in order):
The buffer protocol, describedin the Python C-API documentation.
The
__array_interface__protocol, describedin this page. A precursor to Python’s bufferprotocol, it defines a way to access the contents of a NumPy array from otherC extensions.The
__array__()method, which asks an arbitrary object to convertitself into an array.
For both the buffer and the__array_interface__ protocols, the objectdescribes its memory layout and NumPy does everything else (zero-copy ifpossible). If that’s not possible, the object itself is responsible forreturning andarray from__array__().
DLPack is yet another protocol to convert foreign objectsto NumPy arrays in a language and device agnostic manner. NumPy doesn’t implicitlyconvert objects to ndarrays using DLPack. It provides the functionnumpy.from_dlpack that accepts any object implementing the__dlpack__ methodand outputs a NumPy ndarray (which is generally a view of the input object’s databuffer). ThePython Specification for DLPack page explains the__dlpack__ protocolin detail.
dtype interoperability#
Similar to__array__() for array objects, defining__numpy_dtype__allows a custom dtype object to be interoperable with NumPy.The__numpy_dtype__ must return a NumPy dtype instance (note thatnp.float64 is not a dtype instance,np.dtype(np.float64) is).
New in version 2.4:Before NumPy 2.4 a.dtype attribute was treated similarly. As of NumPy 2.4both is accepted and implementing__numpy_dtype__ prevents.dtypefrom being checked.
The array interface protocol#
Thearray interface protocol defines a way forarray-like objects to reuse each other’s data buffers. Its implementationrelies on the existence of the following attributes or methods:
__array_interface__: a Python dictionary containing the shape, theelement type, and optionally, the data buffer address and the strides of anarray-like object;__array__(): a method returning the NumPy ndarray copy or a view of anarray-like object;
The__array_interface__ attribute can be inspected directly:
>>>importnumpyasnp>>>x=np.array([1,2,5.0,8])>>>x.__array_interface__{'data': (94708397920832, False), 'strides': None, 'descr': [('', '<f8')], 'typestr': '<f8', 'shape': (4,), 'version': 3}
The__array_interface__ attribute can also be used to manipulate the objectdata in place:
>>>classwrapper():...pass...>>>arr=np.array([1,2,3,4])>>>buf=arr.__array_interface__>>>buf{'data': (140497590272032, False), 'strides': None, 'descr': [('', '<i8')], 'typestr': '<i8', 'shape': (4,), 'version': 3}>>>buf['shape']=(2,2)>>>w=wrapper()>>>w.__array_interface__=buf>>>new_arr=np.array(w,copy=False)>>>new_arrarray([[1, 2], [3, 4]])
We can check thatarr andnew_arr share the same data buffer:
>>>new_arr[0,0]=1000>>>new_arrarray([[1000, 2], [ 3, 4]])>>>arrarray([1000, 2, 3, 4])
The__array__() method#
The__array__() method ensures that any NumPy-like object (an array, anyobject exposing the array interface, an object whose__array__() methodreturns an array or any nested sequence) that implements it can be used as aNumPy array. If possible, this will mean using__array__() to create a NumPyndarray view of the array-like object. Otherwise, this copies the data into anew ndarray object. This is not optimal, as coercing arrays into ndarrays maycause performance problems or create the need for copies and loss of metadata,as the original object and any attributes/behavior it may have had, is lost.
The signature of the method should be__array__(self,dtype=None,copy=None).If a passeddtype isn’tNone and different than the object’s data type,a casting should happen to a specified type. Ifcopy isNone, a copyshould be made only ifdtype argument enforces it. Forcopy=True, a copyshould always be made, wherecopy=False should raise an exception if a copyis needed.
If a class implements the old signature__array__(self), fornp.array(a)a warning will be raised saying thatdtype andcopy arguments are missing.
To see an example of a custom array implementation including the use of__array__(), seeWriting custom array containers.
The DLPack Protocol#
TheDLPack protocol defines a memory-layout ofstrided n-dimensional array objects. It offers the following syntaxfor data exchange:
A
numpy.from_dlpackfunction, which accepts (array) objects with a__dlpack__method and uses that method to construct a new arraycontaining the data fromx.__dlpack__(self,stream=None)and__dlpack_device__methods on thearray object, which will be called from withinfrom_dlpack, to querywhat device the array is on (may be needed to pass in the correctstream, e.g. in the case of multiple GPUs) and to access the data.
Unlike the buffer protocol, DLPack allows exchanging arrays containing data ondevices other than the CPU (e.g. Vulkan or GPU). Since NumPy only supports CPU,it can only convert objects whose data exists on the CPU. But other libraries,likePyTorch andCuPy, may exchange data on GPU using this protocol.
2. Operating on foreign objects without converting#
A second set of methods defined by the NumPy API allows us to defer theexecution from a NumPy function to another array library.
Consider the following function.
>>>importnumpyasnp>>>deff(x):...returnnp.mean(np.exp(x))
Note thatnp.exp is aufunc, which meansthat it operates on ndarrays in an element-by-element fashion. On the otherhand,np.mean operates along one of the array’s axes.
We can applyf to a NumPy ndarray object directly:
>>>x=np.array([1,2,3,4])>>>f(x)21.1977562209304
We would like this function to work equally well with any NumPy-like arrayobject.
NumPy allows a class to indicate that it would like to handle computations in acustom-defined way through the following interfaces:
__array_ufunc__: allows third-party objects to support and overrideufuncs.__array_function__: a catch-all for NumPy functionality that is notcovered by the__array_ufunc__protocol for universal functions.
As long as foreign objects implement the__array_ufunc__ or__array_function__ protocols, it is possible to operate on them without theneed for explicit conversion.
The__array_ufunc__ protocol#
Auniversal function (or ufunc for short) is a“vectorized” wrapper for a function that takes a fixed number of specific inputsand produces a fixed number of specific outputs. The output of the ufunc (andits methods) is not necessarily a ndarray, if not all input arguments arendarrays. Indeed, if any input defines an__array_ufunc__ method, controlwill be passed completely to that function, i.e., the ufunc is overridden. The__array_ufunc__ method defined on that (non-ndarray) object has access tothe NumPy ufunc. Because ufuncs have a well-defined structure, the foreign__array_ufunc__ method may rely on ufunc attributes like.at(),.reduce(), and others.
A subclass can override what happens when executing NumPy ufuncs on it byoverriding the defaultndarray.__array_ufunc__ method. This method isexecuted instead of the ufunc and should return either the result of theoperation, orNotImplemented if the operation requested is not implemented.
The__array_function__ protocol#
To achieve enough coverage of the NumPy API to support downstream projects,there is a need to go beyond__array_ufunc__ and implement a protocol thatallows arguments of a NumPy function to take control and divert execution toanother function (for example, a GPU or parallel implementation) in a way thatis safe and consistent across projects.
The semantics of__array_function__ are very similar to__array_ufunc__,except the operation is specified by an arbitrary callable object rather than aufunc instance and method. For more details, seeNEP 18 — A dispatch mechanism for NumPy’s high level array functions.
3. Returning foreign objects#
A third type of feature set is meant to use the NumPy function implementationand then convert the return value back into an instance of the foreign object.The__array_finalize__ and__array_wrap__ methods act behind the scenesto ensure that the return type of a NumPy function can be specified as needed.
The__array_finalize__ method is the mechanism that NumPy provides to allowsubclasses to handle the various ways that new instances get created. Thismethod is called whenever the system internally allocates a new array from anobject which is a subclass (subtype) of the ndarray. It can be used to changeattributes after construction, or to update meta-information from the “parent.”
The__array_wrap__ method “wraps up the action” in the sense of allowing anyobject (such as user-defined functions) to set the type of its return value andupdate attributes and metadata. This can be seen as the opposite of the__array__ method. At the end of every object that implements__array_wrap__, this method is called on the input object with the highestarray priority, or the output object if one was specified. The__array_priority__ attribute is used to determine what type of object toreturn in situations where there is more than one possibility for the Pythontype of the returned object. For example, subclasses may opt to use this methodto transform the output array into an instance of the subclass and updatemetadata before returning the array to the user.
For more information on these methods, seeSubclassing ndarray andSpecific features of ndarray sub-typing.
Interoperability examples#
Example: PandasSeries objects#
Consider the following:
>>>importpandasaspd>>>ser=pd.Series([1,2,3,4])>>>type(ser)pandas.core.series.Series
Now,ser isnot a ndarray, but because itimplements the __array_ufunc__ protocol,we can apply ufuncs to it as if it were a 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
We can even do operations with other 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
Example: PyTorch tensors#
PyTorch is an optimized tensor library for deeplearning using GPUs and CPUs. PyTorch arrays are commonly calledtensors.Tensors are similar to NumPy’s ndarrays, except that tensors can run on GPUs orother hardware accelerators. In fact, tensors and NumPy arrays can often sharethe same underlying memory, eliminating the need to copy data.
>>>importtorch>>>data=[[1,2],[3,4]]>>>x_np=np.array(data)>>>x_tensor=torch.tensor(data)
Note thatx_np andx_tensor are different kinds of objects:
>>>x_nparray([[1, 2], [3, 4]])>>>x_tensortensor([[1, 2], [3, 4]])
However, we can treat PyTorch tensors as NumPy arrays without the need forexplicit conversion:
>>>np.exp(x_tensor)tensor([[ 2.7183, 7.3891], [20.0855, 54.5982]], dtype=torch.float64)
Also, note that the return type of this function is compatible with the initialdata type.
Warning
While this mixing of ndarrays and tensors may be convenient, it is notrecommended. It will not work for non-CPU tensors, and will have unexpectedbehavior in corner cases. Users should prefer explicitly converting thendarray to a tensor.
Note
PyTorch does not implement__array_function__ or__array_ufunc__.Under the hood, theTensor.__array__() method returns a NumPy ndarray asa view of the tensor data buffer. Seethis issue and the__torch_function__ implementationfor details.
Note also that we can see__array_wrap__ in action here, even thoughtorch.Tensor is not a subclass of ndarray:
>>>importtorch>>>t=torch.arange(4)>>>np.abs(t)tensor([0, 1, 2, 3])
PyTorch implements__array_wrap__ to be able to get tensors back from NumPyfunctions, and we can modify it directly to control which type of objects arereturned from these functions.
Example: CuPy arrays#
CuPy is a NumPy/SciPy-compatible array library for GPU-accelerated computingwith Python. CuPy implements a subset of the NumPy interface by implementingcupy.ndarray,a counterpart to NumPy ndarrays.
>>>importcupyascp>>>x_gpu=cp.array([1,2,3,4])
Thecupy.ndarray object implements the__array_ufunc__ interface. Thisenables NumPy ufuncs to be applied to CuPy arrays (this will defer operation tothe matching CuPy CUDA/ROCm implementation of the ufunc):
>>>np.mean(np.exp(x_gpu))array(21.19775622)
Note that the return type of these operations is still consistent with theinitial type:
>>>arr=cp.random.randn(1,2,3,4).astype(cp.float32)>>>result=np.sum(arr)>>>print(type(result))<class 'cupy._core.core.ndarray'>
Seethis page in the CuPy documentation for details.
cupy.ndarray also implements the__array_function__ interface, meaningit is possible to do operations such as
>>>a=np.random.randn(100,100)>>>a_gpu=cp.asarray(a)>>>qr_gpu=np.linalg.qr(a_gpu)
CuPy implements many NumPy functions oncupy.ndarray objects, but not all.Seethe CuPy documentationfor details.
Example: Dask arrays#
Dask is a flexible library for parallel computing in Python. Dask Arrayimplements a subset of the NumPy ndarray interface using blocked algorithms,cutting up the large array into many small arrays. This allows computations onlarger-than-memory arrays using multiple cores.
Dask supports__array__() and__array_ufunc__.
>>>importdask.arrayasda>>>x=da.random.normal(1,0.1,size=(20,20),chunks=(10,10))>>>np.mean(np.exp(x))dask.array<mean_agg-aggregate, shape=(), dtype=float64, chunksize=(), chunktype=numpy.ndarray>>>>np.mean(np.exp(x)).compute()5.090097550553843
Note
Dask is lazily evaluated, and the result from a computation isn’t computeduntil you ask for it by invokingcompute().
Seethe Dask array documentationand thescope of Dask arrays interoperability with NumPy arrays for details.
Example: DLPack#
Several Python data science libraries implement the__dlpack__ protocol.Among them arePyTorch andCuPy. A full list of libraries that implementthis protocol can be found onthis page of DLPack documentation.
Convert a PyTorch CPU tensor to NumPy array:
>>>importtorch>>>x_torch=torch.arange(5)>>>x_torchtensor([0, 1, 2, 3, 4])>>>x_np=np.from_dlpack(x_torch)>>>x_nparray([0, 1, 2, 3, 4])>>># note that x_np is a view of x_torch>>>x_torch[1]=100>>>x_torchtensor([ 0, 100, 2, 3, 4])>>>x_nparray([ 0, 100, 2, 3, 4])
The imported arrays are read-only so writing or operating in-place will fail:
>>>x.flags.writeableFalse>>>x_np[1]=1Traceback (most recent call last): File"<stdin>", line1, in<module>ValueError:assignment destination is read-only
A copy must be created in order to operate on the imported arrays in-place, butwill mean duplicating the memory. Do not do this for very large arrays:
>>>x_np_copy=x_np.copy()>>>x_np_copy.sort()# works
Note
Note that GPU tensors can’t be converted to NumPy arrays since NumPy doesn’tsupport GPU devices:
>>>x_torch=torch.arange(5,device='cuda')>>>np.from_dlpack(x_torch)Traceback (most recent call last): File"<stdin>", line1, in<module>RuntimeError:Unsupported device in DLTensor.
But, if both libraries support the device the data buffer is on, it ispossible to use the__dlpack__ protocol (e.g.PyTorch andCuPy):
>>>x_torch=torch.arange(5,device='cuda')>>>x_cupy=cupy.from_dlpack(x_torch)
Similarly, a NumPy array can be converted to a PyTorch tensor:
>>>x_np=np.arange(5)>>>x_torch=torch.from_dlpack(x_np)
Read-only arrays cannot be exported:
>>>x_np=np.arange(5)>>>x_np.flags.writeable=False>>>torch.from_dlpack(x_np)Traceback (most recent call last): File"<stdin>", line1, in<module> File".../site-packages/torch/utils/dlpack.py", line63, infrom_dlpackdlpack=ext_tensor.__dlpack__()TypeError:NumPy currently only supports dlpack for writeable arrays
Further reading#
Special attributes and methods (details on the
__array_ufunc__and__array_function__protocols)Subclassing ndarray (details on the
__array_wrap__and__array_finalize__methods)Specific features of ndarray sub-typing (more details on the implementation of
__array_finalize__,__array_wrap__and__array_priority__)