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 需要了解 ufuncs,因此,我们存在一个循环依赖,而不是一个干净的分层。为解决此问题,multiarray 导出一个有些可怕的函数,称为 set_numeric_ops。每次您 import numpy 时的引导程序是:

  1. multiarray 及其 ndarray 对象已加载,但对 ndarray 的算术运算已损坏。

  2. umath 已加载。

  3. set_numeric_ops 用于用 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 对象,但我们可以使其中一个或两个模块成为很小的垫片,只需重新导出魔术 API 对象即可(它实际上是在何处定义的)。(请参见 numpy/core/code_generators/generate_{numpy,ufunc}_api.py,了解这些导入的工作原理的详细信息。)

向后兼容性#

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

拒绝备选方案#

保留 set_numeric_ops 以供 monkeypatching 使用#

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

讨论#