NEP 30 — NumPy 数组的鸭子类型 - 实现#

作者:

Peter Andreas Entschev <pentschev@nvidia.com>

作者:

Stephan Hoyer <shoyer@google.com>

状态:

已取代

被取代者:

NEP 56 — NumPy 主命名空间中对数组 API 标准的支持

类型:

标准跟踪

创建日期:

2019-07-31

更新日期:

2019-07-31

决议:

https://mail.python.org/archives/list/numpy-discussion@python.org/message/Z6AA5CL47NHBNEPTFWYOTSUVSRDGHYPN/

摘要#

我们提议 __duckarray__ 协议,它遵循 NEP 22 中描述的高层概述,允许下游库返回其定义类型的数组,这与将 array_like 对象强制转换为 NumPy 数组的 np.asarray 不同。

详细描述#

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 中。如果存在,则有理由期望任何鸭子数组都能在此处工作。如果不存在,我们建议您在文档字符串中指明接受哪种鸭子数组,或者它们需要具备哪些属性。

为了举例说明鸭子数组的用法,假设我们想获取一个类似数组对象 arrmean()。使用 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 数组的项目的示例#

现在考虑一个实现了 NumPy 兼容数组类 NumPyLikeArray 的库,该类应实现上述方法,其完整实现将如下所示

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__ 方法(如果对未实现 __array__ 的任意对象调用 np.asarray,它将创建一个 NumPy 数组标量)。

对于尚未实现 __array__ 但希望使用鸭子数组类型化的现有库,建议它们同时引入 __array____duckarray__ 方法。

用法#

以下展示了如何使用 __duckarray__ 协议编写一个基于 concatenatestack 函数及其产生的结果。此处选择该示例不仅是为了演示 duckarray 函数的用法,也是为了通过对数组 shape 属性的检查来演示其对 NumPy API 的依赖。请注意,此示例仅是 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.arraynp.asarray 函数将数组强制转换回 NumPy 数组的能力,从而防止此类数组被强制转换回纯 NumPy 数组的意外影响(某些库已经这样做,例如 CuPy 和 Sparse),但仍为未实现该协议的库保留了选择使用 np.duckarrayarray_like 对象提升为纯 NumPy 数组的选项。

先前提案和讨论#

此处提出的鸭子类型协议已在 NEP 22 中进行了高层描述。

此外,关于该协议和相关提案的更长时间的讨论发生在 numpy/numpy #13831