NEP 30 — NumPy 数组的鸭子类型 - 实现#
- 作者:
Peter Andreas Entschev <pentschev@nvidia.com>
- 作者:
Stephan Hoyer <shoyer@google.com>
- 状态:
已废弃
- 已替换:
- 类型:
标准轨道
- 创建时间:
2019-07-31
- 更新于:
2019-07-31
- 解决时间:
摘要#
我们提出了 __duckarray__ 协议,遵循 NEP 22 中描述的高级概述,允许下游库返回其定义的类型的数组,与 np.asarray 相反,后者会将 array_like 对象强制转换为 NumPy 数组。
详细描述#
NumPy 的 API,包括数组定义,在无数其他项目中都有实现和模仿。根据定义,其中许多数组在操作方式上与 NumPy 标准相当相似。引入 __array__function__ 允许直接通过 NumPy 的 API 分派这些项目实现的函数。这引入了一个新要求,即返回 NumPy 类似的数组本身,而不是强制转换为纯 NumPy 数组。
为此目的,NEP 22 引入了 NumPy 数组的鸭子类型概念。NEP 中描述的建议解决方案允许库在必要时避免将 NumPy 类似的数组强制转换为纯 NumPy 数组,同时仍然允许那些不希望实现协议的 NumPy 类似数组库通过 np.asarray 将数组强制转换为纯 NumPy 数组。
使用指南#
使用 np.duckarray 的代码旨在支持其他“遵循 NumPy API”的 ndarray 类对象。目前这是一个定义不明确的概念——每个已知的库都只部分实现了 NumPy API,而且许多库在至少一些细微方面有意偏离。这不容易弥补,因此对于 np.duckarray 的用户,我们推荐以下策略:检查遵循 np.duckarray 使用的代码所使用的 NumPy 功能是否存在于 Dask、CuPy 和 Sparse 中。如果存在,那么可以合理地预期任何鸭子数组在这里都能正常工作。如果不存在,我们建议您在文档字符串中说明接受哪种类型的鸭子数组,或者它们需要具有哪些属性。
为了举例说明鸭子数组的用法,假设我们想计算一个类数组对象 arr 的 mean()。使用 NumPy 来实现这一点,可以编写 np.asarray(arr).mean() 来达到预期的结果。如果 arr 不是 NumPy 数组,这将创建一个实际的 NumPy 数组以调用 .mean()。然而,如果数组是符合 NumPy API(无论是完全还是部分)的对象,如 CuPy、Sparse 或 Dask 数组,那么这种复制本就是不必要的。另一方面,如果使用新的 __duckarray__ 协议:np.duckarray(arr).mean(),并且 arr 是符合 NumPy API 的对象,它将被直接返回,而不是强制转换为纯 NumPy 数组,从而避免不必要的复制和潜在的性能损失。
实现#
实现思路相当直接,需要在 NumPy 中引入一个新的函数 duckarray,并在 NumPy 类似的数组类中引入一个新的方法 __duckarray__。新的 __duckarray__ 方法应返回下游类数组对象本身,例如 self 对象,而 __array__ 方法将引发 TypeError。或者,__array__ 方法可以创建一个实际的 NumPy 数组并返回它。
新的 NumPy duckarray 函数可以如下实现
def duckarray(array_like):
if hasattr(array_like, '__duckarray__'):
return array_like.__duckarray__()
return np.asarray(array_like)
实现 NumPy 类似数组的项目的示例#
现在考虑一个实现了名为 NumPyLikeArray 的 NumPy 兼容数组类的库,该类应实现上述方法,并且完整的实现如下
class NumPyLikeArray:
def __duckarray__(self):
return self
def __array__(self):
raise TypeError("NumPyLikeArray can not be converted to a NumPy "
"array. You may want to use np.duckarray() instead.")
上面的实现例证了最简单的情况,但总体思路是库将实现一个返回原始对象的 __duckarray__ 方法,以及一个创建并返回适当的 NumPy 数组(或引发 TypeError 以防止意外用作 NumPy 数组中的对象,如果对不实现 __array__ 的任意对象调用 np.asarray,它将创建一个 NumPy 数组标量)的 __array__ 方法。
对于现有库,如果它们尚未实现 __array__ 但希望使用鸭子数组类型,建议它们同时引入 __array__ 和 __duckarray__ 方法。
用法#
一个如何使用 __duckarray__ 协议编写基于 concatenate 的 stack 函数的示例,以及其产生的输出,可以在下面看到。此示例的选择不仅是为了演示 duckarray 函数的用法,而且还为了演示其对 NumPy API 的依赖,通过检查数组的 shape 属性来证明。请注意,此示例只是 NumPy 实际实现 stack 在第一个轴上工作的简化版本,并假定 Dask 已实现 __duckarray__ 方法。
def duckarray_stack(arrays):
arrays = [np.duckarray(arr) for arr in arrays]
shapes = {arr.shape for arr in arrays}
if len(shapes) != 1:
raise ValueError('all input arrays must have the same shape')
expanded_arrays = [arr[np.newaxis, ...] for arr in arrays]
return np.concatenate(expanded_arrays, axis=0)
dask_arr = dask.array.arange(10)
np_arr = np.arange(10)
np_like = list(range(10))
duckarray_stack((dask_arr, dask_arr)) # Returns dask.array
duckarray_stack((dask_arr, np_arr)) # Returns dask.array
duckarray_stack((dask_arr, np_like)) # Returns dask.array
相比之下,仅使用 np.asarray(在撰写此 NEP 时,这是库开发人员确保数组是 NumPy 类似的常用方法)会产生不同的结果
def asarray_stack(arrays):
arrays = [np.asanyarray(arr) for arr in arrays]
# The remaining implementation is the same as that of
# ``duckarray_stack`` above
asarray_stack((dask_arr, dask_arr)) # Returns np.ndarray
asarray_stack((dask_arr, np_arr)) # Returns np.ndarray
asarray_stack((dask_arr, np_like)) # Returns np.ndarray
向后兼容性#
此提案不会在 NumPy 内部引发任何向后兼容性问题,因为它只引入了一个新函数。但是,选择引入 __duckarray__ 协议的下游库可以选择移除通过 np.array 或 np.asarray 函数将数组强制转换回 NumPy 数组的功能,以防止这些数组意外地强制转换回纯 NumPy 数组(正如一些库已经做的那样,如 CuPy 和 Sparse),但仍然允许未实现该协议的库选择利用 np.duckarray 将 array_like 对象提升为纯 NumPy 数组。
先前提案和讨论#
此处提出的鸭子类型协议在 NEP 22 中以高层级的形式进行了描述。
此外,关于该协议和相关提案的更长远的讨论发生在 numpy/numpy #13831
版权#
本文档已置于公共领域。