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 函数选择其他名称。不幸的是,“array-like”(类数组)已被用于表示“任何可以被强制转换为数组的对象”(包括例如列表对象),而“anyarray”已被用于表示“共享 ndarray 实现但具有不同语义的对象”,这与鸭子数组的概念相反(例如,np.matrix 是一个“anyarray”,但不是一个“duck array”)。这是一个典型的“自行车棚效应”,所以目前我们只使用“鸭子数组”。然而,一些可能的选项包括: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 <XX,待编写>。
协议方法也更符合核心 Python 约定:例如,请参阅用于将对象强制转换为迭代器的 __iter__
方法,或用于安全整数强制转换的 __index__
协议。最后,侧重于协议为部分鸭子数组留下了开放空间,它们可以选择参与哪些协议子集,并且每个子集都有明确定义的语义。
结论:协议是一个非常棒的主意——让我们多做些这样的事。
原则4:尽可能重用现有方法#
尝试定义更简洁的 ndarray 方法版本,以更简洁的接口实现,从而简化实现,这是很诱人的。例如,__array_reshape__
可以去掉 reshape
接受的一些奇怪参数,而 __array_basic_getitem__
可以去掉 NumPy 高级索引的 所有奇怪的边缘情况。
但如上所述,我们并不真正知道鸭子类型化 ndarray 需要哪些 API。我们最终将不可避免地得到一个非常长的新特殊方法列表。相比之下,像 reshape
和 __getitem__
这样的现有方法具有已被使用鸭子数组的库广泛使用/实践的优点,实际上,任何重要的鸭子数组类型无论如何都必须实现它们。
原则5:让正确的事情变得容易#
让鸭子数组良好工作将是一项社区协作努力。文档有所帮助,但作用有限。我们希望让实现正确行为的鸭子数组变得容易。
NumPy 可以通过提供混入(mixin)类来一次性实现大批相关功能,从而提供帮助。NDArrayOperatorsMixin
就是一个很好的例子:它允许通过 __array_ufunc__
方法隐式实现算术运算符。它还不完善,我们需要更多这样的辅助工具(例如,用于归约操作)。
(我们最初认为这些 mixin 的重要性可能是提供一个数组 ABC 的一个论据,因为这是现代 Python 中实现 mixin 的标准方式。但在围绕 NEP 16 的讨论中,我们意识到部分鸭子数组在某些情况下也想利用这些 mixin,所以即使我们确实有一个数组 ABC,这些 mixin 仍然需要某种独立的存在。所以不用管那个论点了。)
暂定的鸭子数组指南#
一般而言,使用鸭子数组的库应坚持最低可能的要求,而实现鸭子数组的库应提供尽可能完整的 API。这将确保最大兼容性。例如,用户应优先依赖 .transpose()
而不是 .swapaxes()
(后者可以通过 transpose 实现),但鸭子数组的作者理想情况下应同时实现两者。
如果您正在尝试实现一个鸭子数组,那么您应该努力实现所有功能。您当然需要 .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 的主题。
版权#
本文档已置于公共领域。