NEP 22 — NumPy 数组的鸭子类型 – 高级概述#
- 作者:
Stephan Hoyer <shoyer@google.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,以及基于步长内存存储的 API 的具体实现。该 API 功能强大、相当通用,并在整个科学 Python 堆栈中广泛使用。另一方面,具体的实现适用于各种用途,但也有局限性:随着数据集的增长以及 NumPy 在各种新环境中的使用,步长内存存储策略越来越不合适的情况越来越多,用户发现他们需要稀疏数组、延迟计算数组(如在 dask 中)、压缩数组(如在 blosc 中)、存储在 GPU 内存中的数组、存储在 Arrow 等替代格式中的数组等等——但用户仍然希望使用熟悉的 NumPy API 来处理这些数组,并以最少(理想情况下为零)的移植开销重用现有代码。作为一种简写,我们称这些为“鸭子数组”,类似于 Python 的“鸭子类型”:一个“鸭子数组”是一个 Python 对象,它“像”numpy 数组一样“嘎嘎叫”,因为它具有相同或相似的 Python API,但不共享 C 级实现。
此 NEP 没有提出对 NumPy 或其他项目的任何具体更改;相反,它概述了我们希望如何扩展 NumPy 以支持实现和依赖其高级 API 的强大的项目生态系统。
术语#
“鸭子数组”目前作为占位符效果很好,但它非常专业,可能会让新用户感到困惑,因此我们可能需要为实际的 API 函数选择其他名称。“类数组”已经被用来表示“任何可以强制转换为数组的内容”(包括例如列表对象),而“anyarray”已经被用来表示“与 ndarray 的实现共享,但语义不同的东西”,这与鸭子数组相反(例如,np.matrix 是一个“anyarray”,但不是一个“鸭子数组”)。这是一个经典的“自行车棚”问题,因此目前我们只使用“鸭子数组”。不过,一些可能的选项包括:arrayish、pseudoarray、nominalarray、ersatzarray、arraymimic……
总体方法#
在高级别上,鸭子数组支持需要处理 NumPy 提供的每个 API 函数,并弄清楚如何将其扩展以与鸭子数组对象一起使用。在某些情况下这很容易(例如,ndarray 本身的方法/属性);在其他情况下则比较困难。以下是一些我们迄今为止发现有用的原则
原则 1:关注“完整”鸭子数组,但不排除“部分”鸭子数组#
我们可以区分两类
“完整”鸭子数组,力求完全实现 np.ndarray 的 Python 级 API,并且基本上可以在 np.ndarray 起作用的任何地方起作用
“部分”鸭子数组,故意只实现 np.ndarray API 的子集。
完整的鸭子数组,嗯,有点无聊。它们与 ndarray 的语义完全相同,区别仅限于幕后关于如何实际存储数据的决定。那些对使 numpy 更具可扩展性感到兴奋的人,也同样对更改或扩展 numpy 的语义感到兴奋。因此,关于如何最好地支持部分鸭子数组有很多讨论。我们自己也有过这样的错误。
但是,在这一点上,我们认为最佳的总体策略是主要将我们的精力集中在支持完整的鸭子数组上,并且只在我们需要确保不会无缘无故地意外排除部分鸭子数组时才考虑它们。
为什么关注完整的鸭子数组?有几个原因
首先,有很多非常明确的用例。完整鸭子数组接口的潜在使用者包括几乎所有使用 numpy 的包(scipy、sklearn、astropy……),尤其是提供处理多种类型数组的数组包装类的包,例如 xarray 和 dask.array。完整鸭子数组接口的潜在实现者包括:分布式数组、稀疏数组、掩码数组、带单位的数组(除非它们切换到使用 dtype)、带标签的数组等等。明确的用例带来了良好且相关的 API。
其次,安娜·卡列尼娜原则在这里适用:完整的鸭子数组都一样,但每个部分鸭子数组都是以其自身的方式不完整
xarray.DataArray
主要是一个鸭子数组,但具有不兼容的广播语义。xarray.Dataset
将多个数组包装在一个对象中;它仍然实现一些数组接口,例如__array_ufunc__
,但肯定不是全部。pandas.Series
具有与 numpy 类似行为的方法,但具有独特的跳过空值行为。scipy 的
LinearOperator
支持矩阵乘法,仅此而已h5py 和类似用于访问数组存储的库具有支持类似 numpy 的切片和转换为完整数组的对象,但不支持计算。
某些类可能与 ndarray 类似,但不支持完整的索引语义。
等等。
尽管我们尽了最大努力,但我们还没有找到一种清晰、独特的方法将 ndarray API 分解成相关的类型层次结构来捕捉这些区别;事实上,不太可能有人完全理解所有这些区别。这一点很重要,因为我们需要为大量的 API 添加鸭子数组支持(在 numpy 中以及所有依赖 numpy 的项目中!)。根据定义,这些 API 已经适用于 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
的实际类型不仅包括其方法和属性,还包括所有这些函数。为了使鸭子数组成功,它们需要实现很大一部分 ndarray
API——但不是全部。(例如,dask.array.Array
没有提供等效于 ndarray.ptp
方法的内容,大概是没有人注意到或在意它的缺失。但这似乎并没有阻止人们使用 dask。)
这意味着实际上,我们无法希望预先定义整个鸭子数组 API,或者任何人能够一次性实现它;这将是一个渐进的过程。这也意味着即使是所谓的“完整”鸭子数组接口在边界上也有些模糊地定义;np.ndarray
API 的某些部分鸭子数组不必实现,但我们并不完全确定它们是什么。
最终,定义什么符合或不符合鸭子数组的标准并不真正取决于 NumPy 开发人员。如果我们希望 scikit-learn 函数适用于 dask 数组(例如),那么这将需要这两个项目之间的协商来发现不兼容性,并且当发现不兼容性时,他们将需要协商谁应该更改以及如何更改。NumPy 项目可以提供技术工具和一般建议来帮助解决这些分歧,但我们不能强迫任何一方对任何特定错误负责。
因此,即使我们专注于“完整”鸭子数组,我们也不尝试定义规范的“数组 ABC”——也许这将来会有用,但现在还没有。作为一个方便的副作用,缺乏规范定义为部分鸭子数组留下了实验的空间。
但是,我们在下面为鸭子数组实现者和使用者提供了一些更详细的建议。
原则 3:专注于协议#
历史上,numpy 通过定义协议(如 __array__
(要求任意对象将其自身转换为数组)、__array_interface__
(Python 缓冲区协议的前身)和 __array_ufunc__
(允许第三方对象支持像 np.exp
这样的 ufunc))在与第三方对象互操作方面取得了巨大成功。
NEP 16 采取了不同的方法:我们需要 asarray
的鸭子数组等效项,它建议通过定义 asarray
的一个版本来实现这一点,该版本将允许实现新的 AbstractArray ABC 的对象通过。如上所述,我们现在认为出于其他原因,尝试定义 ABC 不是一个好主意。但是,当在邮件列表中讨论这个 NEP 时,我们意识到即使就其本身而言,这个想法也不是很好。更好的方法是定义一个可以调用任意对象上的方法来要求它将其自身转换为鸭子数组,然后定义 asarray
的一个版本来调用此方法。
这严格来说更强大:如果一个对象已经是鸭子数组,它可以简单地 return self
。它允许更正确的语义:NEP 16 假设 asarray(obj, dtype=X)
与 asarray(obj).astype(X)
相同,但这并不正确。它支持更多用例:如果 h5py 支持稀疏数组,它可能希望提供一个本身不是稀疏数组的对象,但可以自动转换为稀疏数组。有关完整详细信息,请参阅 NEP
协议方法也与核心 Python 约定更一致:例如,参见用于将对象强制转换为迭代器的 __iter__
方法,或用于安全整数强制转换的 __index__
协议。最后,专注于协议为部分鸭子数组留下了空间,它们可以选择参与哪些协议子集,每个协议子集都有明确定义的语义。
结论:协议是一个很棒的主意——让我们多做一些这样的事情。
原则 4:尽可能重用现有方法#
尝试定义具有更简洁接口的 ndarray 方法的清理版本以方便实现是很诱人的。例如,__array_reshape__
可以删除 reshape
接受的一些奇怪的参数,而 __array_basic_getitem__
可以删除 NumPy 高级索引的所有奇怪的边缘情况。
但如上所述,我们并不真正知道鸭子类型 ndarray 需要哪些 API。我们最终将不可避免地得到一个非常长的新的特殊方法列表。相反,像 reshape
和 __getitem__
这样的现有方法具有已经被使用/练习的优势,实际上,任何严肃的鸭子数组类型都必须实现它们。
原则 5:使正确操作变得容易#
使鸭子数组正常工作将是一项社区努力。文档有所帮助,但作用有限。我们希望简化实现能够正确工作的鸭子数组的过程。
NumPy 可以提供帮助的一种方式是提供混合类,以便一次性实现大量相关功能。NDArrayOperatorsMixin
就是一个很好的例子:它允许通过 __array_ufunc__
方法隐式地实现算术运算符。它并不完整,我们希望有更多这样的辅助程序(例如,用于归约)。
(我们最初认为这些混合类的重要性可能是提供数组 ABC 的一个论据,因为这是在现代 Python 中使用混合类的标准方法。但在围绕 NEP 16 的讨论中,我们意识到部分鸭子数组在某些情况下也希望利用这些混合类,因此即使我们确实有数组 ABC,混合类仍然需要某种单独的存在。所以别再理会那个论据了。)
初步鸭子数组指南#
一般来说,使用鸭子数组的库应该坚持最低限度的要求,而实现鸭子数组的库应该提供尽可能完整的 API。这将确保最大的兼容性。例如,用户应该更倾向于依赖 .transpose()
而不是 .swapaxes()
(可以用 transpose 实现),但鸭子数组作者应该理想情况下同时实现两者。
如果您尝试实现鸭子数组(duck array),则应努力实现所有功能。您肯定需要.shape
、.ndim
和.dtype
,但您的dtype属性也应该实际上是一个numpy.dtype
对象,理想情况下应该支持奇特的索引边缘情况等等。只有与NumPy的特定np.ndarray
实现相关的细节(例如,strides
、data
、view
)才明确地不在考虑范围内。
未来计划(非常)粗略的草图#
到目前为止讨论的提案——__array_ufunc__
和某种asarray
协议——显然是必要的,但不足以完全支持鸭子类型。我们预计需要额外的协议来支持(至少)以下这些功能
**连接**鸭子数组,这将被其他数组组合方法(如stack/vstack/hstack)内部使用。concatenate的实现需要在数组参数列表之间协商。我们预计将使用类似于
__array_ufunc__
的__array_concatenate__
协议,而不是多重分派。**类似ufunc的函数**,这些函数目前不是ufunc。许多NumPy函数,如median、percentile、sort、where和clip,可以编写为广义ufunc,但目前还不是。这些函数应该编写为ufunc,或者我们应该考虑添加另一种通用的包装机制,该机制的工作方式类似于ufunc,但对实现方式的保证更少。
使用鸭子数组进行**随机数生成**,例如
np.random.randn()
。例如,我们可能希望添加新的API,例如random_like()
,用于生成具有匹配形状*和*类型的新的数组——尽管我们需要查看这些函数的一些实际示例,以弄清楚什么会有帮助。**其他各种函数**,例如
np.einsum
、np.zeros_like
和np.broadcast_to
,这些函数不属于上述任何类别。**检查鸭子数组的可变性**,这意味着它们支持使用
__setitem__
进行赋值以及ufunc的out参数。许多其他很好的鸭子数组不容易改变(例如,因为它们使用某种稀疏或压缩存储,或者位于只读共享内存中),并且事实证明,经常使用的代码(例如np.mean
的默认实现)需要检查这一点(以决定是否可以重用临时数组)。
我们故意没有在此处详细说明如何添加对这些类型鸭子数组的支持。这些将是未来NEP的主题。
版权#
本文档已进入公共领域。