NEP 22 — NumPy 数组的鸭子类型 — 高级概述#

作者

斯蒂芬·霍耶 < shoyer @谷歌 com >,Nathaniel J. Smith < njs @ pobox com >

地位

最终的

类型

信息性

创建

2018-03-22

解决

https://mail.python.org/pipermail/numpy-discussion/2018-September/078752.html

抽象的

我们概述了 NumPy 如何处理“鸭子数组”的高级愿景。这是一个信息级 NEP;它没有规定任何特定实现的完整细节。简而言之,我们建议开发一些新协议,用于定义具有与 NumPy 匹配的高级 API 的多维数组的实现。

详细说明

传统上,NumPy 的ndarray对象提供了两件事:用于对同质类型、任意维度、数组结构数据进行表达式操作的高级 API,以及基于跨步 RAM 存储的 API 的具体实现。该 API 功能强大、相当通用,并且在科学 Python 堆栈中广泛使用。另一方面,具体实现适用于广泛的用途,但也有局限性:随着数据集的增长和 NumPy 在各种新环境中的使用,越来越多的情况采用跨步式 RAM 存储策略不合适,用户发现他们需要稀疏数组、延迟计算数组(如 dask 中)、压缩数组(如 blosc 中)、存储在 GPU 内存中的数组、以其他格式存储的数组(如 Arrow 等),但用户仍然想要使用熟悉的 NumPy API 处理这些数组,并以最小(最好为零)的移植开销重用现有代码。作为一种工作简写,我们将这些称为“鸭子数组”,与 Python 的“鸭子类型”进行类比:“鸭子数组”是一个 Python 对象,它“像”numpy 数组,因为它具有相同或相似的 Python 语言API,但不共享 C 级实现。

本 NEP 不建议对 NumPy 或其他项目进行任何具体更改;相反,它概述了我们希望如何扩展 NumPy 以支持实施和依赖其高级 API 的强大项目生态系统。

术语#

“Duck array”目前作为占位符工作得很好,但它相当术语,可能会让新用户感到困惑,所以我们可能想为实际的 API 函数选择其他东西。不幸的是,“类数组”已经被用来表示“任何可以强制转换为数组的东西”(包括例如列表对象)的概念,并且“anyarray”已经被用来表示“共享 ndarray 实现的东西,但是具有不同的语义”,这与鸭子数组相反(例如,np.matrix 是“anyarray”,但不是“鸭子数组”)。这是一个经典的自行车棚,所以现在我们只使用“鸭子阵列”。一些可能的选项包括:arrayish、pseudoarray、nominalarray、ersatzarray、arraymimic,...

一般的做法

在较高层面上,鸭子数组支持需要研究 NumPy 提供的每个 API 函数,并弄清楚如何扩展它以使用鸭子数组对象。在某些情况下,这很容易(例如,ndarray 本身的方法/属性);在其他情况下则更困难。以下是迄今为止我们发现有用的一些原则:

原则 1:关注“完整”鸭子数组,但不排除“部分”鸭子数组#

我们可以区分两个类:

  • “完整”duck 数组,渴望完全实现 np.ndarray 的 Python 级 API,并且基本上可以在 np.ndarray 工作的任何地方工作

  • “部分”鸭子数组,有意仅实现 np.ndarray API 的子集。

完整的鸭子阵列有点无聊。它们具有与 ndarray 完全相同的语义,差异仅限于有关数据实际存储方式的底层决策。毫不奇怪,那些对让 numpy 更具可扩展性感到兴奋的人也对改变或扩展 numpy 的语义感到兴奋。因此,关于如何最好地支持部分鸭子数组进行了很多讨论。我们自己也曾犯过这样的罪。

但在这一点上,我们认为最好的总体策略是将我们的努力主要集中在支持完整的鸭子阵列上,并且只担心部分鸭子阵列,因为我们需要确保我们不会无缘无故地意外排除它们。

为什么要关注完整的鸭子阵列?几个原因:

首先,有很多非常明确的用例。完整的 duck 数组接口的潜在使用者包括几乎所有使用 numpy 的包(scipy、sklearn、astropy 等),特别是提供处理多种类型数组的数组包装类的包,例如 xarray 和 dask.array 。完整鸭子数组接口的潜在实现者包括:分布式数组、稀疏数组、掩码数组、带单位的数组(除非它们切换到使用 dtypes)、标记数组等。清晰的用例会带来良好且相关的 API。

其次,安娜·卡列尼娜原则适用于此:完整的鸭子数组都是相似的,但每个部分鸭子数组都有自己的部分:

  • xarray.DataArray主要是一个鸭子数组,但具有不兼容的广播语义。

  • xarray.Dataset将多个数组包装在一个对象中;它仍然实现一些数组接口,例如__array_ufunc__,但肯定不是全部。

  • pandas.Series具有与 numpy 类似行为的方法,但具有独特的空值跳过行为。

  • scipy 的LinearOperator支持矩阵乘法,仅此而已

  • h5py 和用于访问数组存储的类似库具有支持类似 numpy 的切片和转换为完整数组的对象,但不支持计算。

  • 有些类可能与 ndarray 类似,但不支持完整的索引语义。

等等。

尽管我们尽了最大努力,但我们还没有找到任何清晰、独特的方法将 ndarray API 分割成相关类型的层次结构来捕获这些区别;事实上,任何一个人都不可能理解所有的区别。这很重要,因为我们有很多API需要添加鸭子数组支持(无论是在 numpy 中还是在所有依赖 numpy 的项目中!)。根据定义,这些已经适用于ndarray,因此希望让它们适用于完整的鸭子数组应该不会那么难,因为根据定义,完整的鸭子数组的行为就像ndarray。如果必须遍历每个函数并确定它所需的 ndarray API 的确切子集,然后找出哪些部分数组类型可以/应该支持它,那将是非常麻烦的。一旦我们完成了完整鸭子数组的工作,我们就可以稍后返回并根据需要进一步完善所需的 API。专注于完整的鸭子阵列使我们能够立即开始取得进展。

将来,识别鸭子数组的特定用例并标准化仅针对这些用例的更窄接口可能会很有用。例如,拥有一个标准的“数组加载器”接口可能有意义,该接口可以实现 h5py、netcdf、pydap、zarr 等文件访问库,以便在这些库之间轻松切换。但这是我们可以随时做的事情,而且根本不需要 NumPy 开发人员的参与。有关其外观的示例,请参阅 dask.array.from_array的文档。

原则 2:利用鸭子打字#

ndarray具有非常大的 API 表面积:

In [1]: len(set(dir(np.ndarray)) - set(dir(object)))
Out[1]: 138

这是一个巨大的低估,因为 NumPy 和其他库中也有许多独立函数,这些函数当前使用 NumPy C API,因此仅适用于ndarray对象。在类型理论中,类型是由可以对对象执行的操作来定义的;因此, 的实际类型ndarray不仅包括其方法和属性,还包括所有这些函数。为了让鸭子数组取得成功,他们需要实现大部分 API ndarray,但不是全部。 (例如, dask.array.Array没有提供与该 ndarray.ptp方法等效的方法,大概是因为没有人注意到或关心它的缺失。但这似乎并没有阻止人们使用 dask。)

这意味着实际上,我们不能希望预先定义整个鸭子数组 API,或者任何人都能够一次性实现它;这将是一个渐进的过程。这也意味着,即使是所谓的“完整”鸭子数组接口,其边界定义也有些模糊;np.ndarray鸭子数组不必实现 API的某些部分,但我们并不完全确定这些部分是什么。

最终,真正由 NumPy 开发人员来定义什么符合或不符合鸭子数组的条件。例如,如果我们希望 scikit-learn 函数在 dask 数组上工作,那么这将需要这两个项目之间进行协商以发现不兼容性,当发现不兼容性时,将由他们协商谁应该更改以及如何更改。 NumPy 项目可以提供技术工具和一般建议来帮助解决这些分歧,但我们不能强迫一个或另一个小组对任何给定的错误承担责任。

因此,即使我们关注“完整”鸭子数组,我们 也不会尝试定义一个规范的“数组 ABC”——也许有一天这会有用,但现在还没有。作为一个方便的副作用,缺乏规范的定义给部分鸭子阵列留下了实验的空间。

但是,我们确实在下面为鸭子数组实现者和消费者提供了一些更详细的建议。

原则 3:关注协议#

从历史上看,numpy 通过定义协议在与第三方对象的互操作方面取得了很多成功,例如__array__(要求任意对象将自身转换为数组)、 __array_interface__(Python 缓冲区协议的前身)和 __array_ufunc__(允许第三方对象支持 ufunc 等 np.exp)。

NEP 16采用了不同的方法:我们需要一个相当于 的鸭子数组 asarray,并且它建议通过定义一个 asarray允许通过实现新 AbstractArray ABC 的对象的版本来实现这一点。如上所述,我们现在认为由于其他原因尝试定义 ABC 是一个坏主意。但当这个 NEP 在邮件列表上讨论时,我们意识到,即使就其本身的优点而言,这个想法也不是那么好。更好的方法是定义一个 可以在任意对象上调用的方法,要求该对象将自身转换为鸭子数组,然后定义一个 asarray调用该方法的版本。

这严格来说更强大:如果一个对象已经是一个鸭子数组,它可以简单地.它允许更正确的语义:NEP 16 假设与 相同 ,但事实并非如此。而且它支持更多的用例:如果h5py支持稀疏数组,它可能希望提供一个本身不是稀疏数组,但可以自动转换为稀疏数组的对象。完整详细信息请参阅 NEP <XX,待编写>。return selfasarray(obj, dtype=X)asarray(obj).astype(X)

该协议方法也更符合 Python 核心约定:例如,请参阅将__iter__对象强制为迭代器的方法,或__index__安全整数强制的协议。最后,关注协议为部分鸭子数组敞开了大门,它们可以挑选和选择他们想要参与的协议子集,每个子​​集都有明确定义的语义。

结论:协议是一个非常棒的想法——让我们做更多这样的事情。

原则 4:尽可能重用现有方法#

人们很容易尝试使用更简单的接口来定义 ndarray 方法的清理版本,以方便实现。例如,__array_reshape__可以删除 NumPy 高级索引所接受的一些奇怪的参数,reshape并可以删除NumPy 高级索引的__array_basic_getitem__ 所有奇怪的边缘情况。

但正如上面所讨论的,我们并不真正知道鸭子类型 ndarray 需要哪些 API。我们不可避免地会得到一长串新的特殊方法。相比之下,诸如reshape 和之类的现有方法__getitem__的优点是已经被使用鸭子数组的库广泛使用/练习,并且在实践中,任何严肃的鸭子数组类型都必须实现它们。

原则 5:让做正确的事变得容易#

让鸭子阵列正常工作需要社区的努力。文档有帮助,但也仅限于此。我们希望能够轻松实现执行正确操作的鸭子数组。

NumPy 可以提供帮助的一种方法是提供 mixin 类来同时实现大量相关功能。 NDArrayOperatorsMixin是一个很好的例子:它允许通过方法隐式实现算术运算符 __array_ufunc__。它并不完整,我们需要更多这样的助手(例如用于减少)。

(我们最初认为这些 mixins 的重要性可能是提供数组 ABC 的一个论点,因为这是在现代 Python 中进行 mixins 的标准方法。但在围绕 NEP 16 的讨论中,我们意识到部分鸭子数组也想利用在某些情况下,这些混入,所以即使我们确实有一个数组 ABC,那么混入仍然需要某种单独的存在,所以不用介意这个论点。)

暂定鸭子阵列指南#

作为一般规则,使用鸭子数组的库应坚持尽可能最低的要求,并且实现鸭子数组的库应提供尽可能完整的 API。这将确保最大的兼容性。例如,用户应该更喜欢依赖 .transpose()而不是.swapaxes()(可以通过转置来实现),但鸭子数组作者理想情况下应该同时实现两者。

如果您尝试实现鸭子数组,那么您应该努力实现所有内容。您当然需要.shape,.ndim.dtype,但您的 dtype 属性实际上应该是一个 numpy.dtype对象,奇怪的奇特索引边缘情况应该理想地工作,等等。只有与 NumPy 的特定实现相关的细节np.ndarray (例如,strides, data, view)明确超出范围。

未来计划的(非常)粗略草图#

到目前为止讨论的提案__array_ufunc__以及某种 asarray协议显然是必要的,但对于完整的鸭子类型支持来说还不够。我们预计需要额外的协议来支持(至少)这些功能:

  • 连接鸭子数组,将由其他数组组合方法(如 stack/vstack/hstack)在内部使用。连接的实现需要在数组参数列表之间进行协商。我们希望使用__array_concatenate__类似的协议__array_ufunc__而不是多重调度。

  • 目前不是 ufunc 的类似 Ufunc 的函数。许多 NumPy 函数(如中位数、百分位数、排序、where 和 Clip)可以编写为广义 ufunc,但目前还没有。这些函数要么应该编写为 ufunc,要么我们应该考虑添加另一个通用包装器机制,其工作方式与 ufunc 类似,但对如何完成实现的保证较少。

  • 使用鸭子数组生成随机数,例如 np.random.randn().例如,我们可能想要添加新的 API,例如random_like()用于生成具有匹配形状类型的新数组 - 尽管我们需要查看一些如何使用这些函数的真实示例来找出有用的内容。

  • 其他各种函数,例如np.einsumnp.zeros_like、 和np.broadcast_to不属于上述任何类别。

  • 检查鸭子数组的可变性,这意味着它们支持__setitem__ufuncs 的赋值和 out 参数。许多其他很好的鸭子数组不容易可变(例如,因为它们使用某种稀疏或压缩存储,或者位于只读共享内存中),并且事实证明,频繁使用的代码(例如默认实现)np.mean 需要检查此项(以决定是否可以重用临时数组)。

我们故意不在这里准确描述如何添加对这些类型的鸭子数组的支持。这些将是未来 NEP 的主题。