NEP 30 — NumPy 数组的鸭子类型 - 实现#
- 作者:
Peter Andreas Entschev <pentschev@nvidia.com>
- 作者:
Stephan Hoyer <shoyer@google.com>
- 状态:
已取代
- 被取代:
- 类型:
标准跟踪
- 创建:
2019-07-31
- 更新:
2019-07-31
- 解决方案:
https://mail.python.org/archives/list/[email protected]/message/Z6AA5CL47NHBNEPTFWYOTSUVSRDGHYPN/
摘要#
我们建议使用 __duckarray__
协议,遵循 NEP 22 中描述的高级概述,允许下游库返回其定义类型的数组,与 np.asarray
相反,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__
方法来返回原始对象,以及一个 __array__
方法来创建并返回适当的 NumPy 数组,或者引发 ``TypeError`` 以防止在 NumPy 数组中意外使用该对象(如果 np.asarray
被调用在没有实现 __array__
的任意对象上,它将创建一个 NumPy 数组标量)。
对于那些没有实现 __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 中进行。
版权#
本文件已置于公有领域。