NEP 15 — 合并 multiarray 和 umath#

作者:

Nathaniel J. Smith <njs@pobox.com>

状态:

最终

类型:

标准轨道

创建时间:

2018-02-22

解决时间:

https://mail.python.org/pipermail/numpy-discussion/2018-June/078345.html

摘要#

让我们将 numpy.core.multiarraynumpy.core.umath 合并为一个单一的扩展模块,并弃用 np.set_numeric_ops

背景#

目前,NumPy 的核心 C 代码被拆分为两个独立的扩展模块。

numpy.core.multiarraynumpy/core/src/multiarray/*.c 构建,包含核心数组功能(特别是 ndarray 对象)。

numpy.core.umathnumpy/core/src/umath/*.c 构建,包含 ufunc(通用函数)机制。

这两个模块各自公开独立的 C API,分别通过 import_multiarray()import_umath() 进行访问。其理念是它们应该是独立的模块,其中 multiarray 作为底层,umath 构建在其之上。但在实践中,这已被证明存在问题。

首先,层级结构并不完美:当你编写 ndarray + ndarray 时,这会调用 ndarray.__add__,后者进而调用 ufunc np.add。这意味着 ndarray 需要了解 ufunc —— 因此我们得到的不是清晰的层级,而是循环依赖。为了解决这个问题,multiarray 导出了一个相当可怕的函数,名为 set_numeric_ops。每次执行 import numpy 时的引导过程如下:

  1. multiarray 及其 ndarray 对象被加载,但 ndarray 上的算术运算此时是不可用的。

  2. umath 被加载。

  3. set_numeric_ops 被用于通过 monkeypatch(猴子补丁)将 umath 中的对象注入到 ndarray.__add__ 等所有方法中。

此外,set_numeric_ops 作为公共 API np.set_numeric_ops 公开。

此外,即使这种分层结构确实有效,它最终也会扭曲我们的公共 ABI 结构。近年来,向 multiarray 的“公共”ABI 添加新函数的最常见原因,并非是因为它们真的需要公开或我们期望其他项目使用它们,而是仅仅因为我们需要从 umath 调用它们。这非常令人遗憾,因为它使我们的公共 ABI 不必要地变得臃肿。由于我们永远无法从中移除内容,这造成了持续的维护负担。根据 C 语言的工作方式,你可以拥有对同一扩展模块内所有内容可见的内部 API,或者拥有每个人都可以使用的公共 API;你(很难)拥有一个既能被 NumPy 内部多个扩展模块可见,又不向外部用户公开的 API。

我们还越来越多地将实用代码放入 numpy/core/src/private/,该目录现在包含许多被 #include 两次的文件(一次在 multiarray,一次在 umath)。这非常糟糕,纯粹是为了绕过它们作为独立 C 扩展这一事实的权宜之计。npymath 库也同时包含在两个扩展模块中。

建议的变更#

本 NEP 提出三项变更:

  1. 我们应该开始将 numpy/core/src/multiarray/*.cnumpy/core/src/umath/*.c 一起构建为一个单一的扩展模块。

  2. 我们应该使用一些新的私有 API 来设置 ndarray.__add__ 等方法,而不是使用 set_numeric_ops

  3. 我们应该弃用并最终移除 np.set_numeric_ops

不建议的变更#

我们并不一定建议在源代码组织方面放弃 multiarray/ 和 umath/ 之间的区别:内部组织是有用的!我们只是希望将它们构建在一起成为一个单一的扩展模块。当然,这确实为未来潜在的重构敞开了大门,我们可以根据它们的价值在出现时进行评估。

它也不建议破坏公共 C ABI。我们应该继续提供 import_multiarray()import_umath() 函数——只是现在这两个 ABI 最终都将从同一个 C 库中加载。由于 import_multiarray()import_umath() 的编写方式,我们仍然需要名为 numpy.core.multiarraynumpy.core.umath 的模块,并且它们需要继续导出 _ARRAY_API_UFUNC_API 对象——但我们可以让这两个模块中的一个或两个成为微小的填充层(shims),仅仅从实际定义的地方重新导出 magic API 对象。(关于这些导入如何工作的详细信息,请参阅 numpy/core/code_generators/generate_{numpy,ufunc}_api.py。)

向后兼容性#

唯一的兼容性中断是 np.set_numeric_ops 的弃用。

被拒绝的替代方案#

保留用于 Monkeypatching 的 set_numeric_ops#

在讨论此 NEP 时,提出了 set_numeric_ops 的另一个用例:如果你有一个优化的向量数学库(例如 Intel 的 MKL VML、Sleef 或 Yeppp),那么 set_numeric_ops 可用于通过 monkeypatch 修改 NumPy,以使用这些运算替代 NumPy 内置的向量运算。但是,即使我们认为这是一个好主意,使用 set_numeric_ops 实际上并不是实现这一点的最佳方式。set_numeric_ops 所能做的只是接管 ndarray 上 Python 的语法运算符(+, * 等);它无法影响通过其他 API(例如 np.add)调用的运算,也无法影响没有内置语法的运算(例如 np.exp)。此外,你必须重新实现整个 ufunc 机制,而不是仅仅重新实现核心循环。另一方面,2006 年引入的 PyUFunc_ReplaceLoopBySignature API 允许替换任意 ufunc 的内部循环。这既简单又强大——例如,替换 np.add 的内部循环意味着你的代码将自动同时用于 ndarray + ndarray 和对 np.add 的直接调用。因此,这似乎不是不弃用 set_numeric_ops 的好理由。

讨论#