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。完整鸭子数组接口的潜在实现者包括:分布式数组、稀疏数组、掩码数组、带单位的数组(除非它们切换到使用 dtypes)、标记数组等等。明确的用例会导致良好且相关的 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 <XX,待编写>。
协议方法也更符合核心 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()
(可以根据转置来实现),但鸭子数组作者理想情况下应该实现两者。
如果您尝试实现鸭子数组,那么您应该努力实现所有内容。您当然需要 .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 的主题。
版权#
本文件已进入公有领域。