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.multiarray 是由 numpy/core/src/multiarray/*.c 构建的,包含核心数组功能(特别是 ndarray 对象)。

numpy.core.umath 是由 numpy/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 将所有类似 ndarray.__add__ 的方法用 umath 中的对象进行猴子补丁(monkeypatch)。

此外,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 对象——但我们可以让其中一个或两个模块成为小的垫片(shim),仅仅从实际定义它们的任何地方重新导出神奇的 API 对象。(有关这些导入工作原理的详细信息,请参阅 numpy/core/code_generators/generate_{numpy,ufunc}_api.py。)

向后兼容性#

唯一会破坏兼容性的地方是废弃 np.set_numeric_ops

被拒绝的替代方案#

保留 set_numeric_ops 以用于猴子补丁#

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

讨论#