NEP 52 — NumPy 2.0 的 Python API 清理#

作者:

Ralf Gommers <ralf.gommers@gmail.com>

作者:

Stéfan van der Walt <stefanv@berkeley.edu>

作者:

Nathan Goldbaum <ngoldbaum@quansight.com>

作者:

Mateusz Sokół <msokol@quansight.com>

状态:

最终

类型:

标准轨迹

创建:

2023-03-28

决议:

https://mail.python.org/archives/list/[email protected]/thread/QLMPFTWA67DXE3JCUQT2RIRLQ44INS4F/

摘要#

我们建议为 NumPy 2.0 版本清理 NumPy 的 Python API。这包括更明确地定义公共 API 和私有 API 之间的划分,并通过删除别名和具有更好替代方案的函数来减少主命名空间的大小。此外,每个函数都应仅从一个位置访问,因此所有重复项也需要删除。

动机和范围#

NumPy 拥有一个庞大的 API 表面,它在多年时间里有机地发展起来

>>> objects_in_api = [s for s in dir(np) if not s.startswith('_')]
>>> len(objects_in_api)
562
>>> modules = [s for s in objects_in_api if inspect.ismodule(eval(f'np.{s}'))]
>>> modules
['char', 'compat', 'ctypeslib', 'emath', 'fft', 'lib', 'linalg', 'ma', 'math', 'polynomial', 'random', 'rec', 'testing', 'version']
>>> len(modules)
14

以上内容甚至不包括那些是公共的但已从 __dir__ 中隐藏的项目。一个特别成问题的例子是 np.core,它在技术上是私有的,但在实践中被大量使用。有关被认为是公共的、私有的或介于两者之间的内容的完整概述,请参阅 numpy/numpy

API 的规模和边界定义的缺乏带来了巨大的成本

  • 用户难以区分名称相似的函数。

    在 IPython、笔记本或 IDE 中使用 Tab 键补全查找函数是一项挑战。例如,键入 np.<TAB> 并查看提供的最初六个项目:两个 ufunc(absadd)、一个别名(absolute)和三个不适用于最终用户的函数(add_docstringadd_newdocadd_newdoc_ufunc)。因此,NumPy 的学习曲线比必要情况更加陡峭。

  • 模仿 NumPy API 的库面临着重大的实现障碍。

    对于 NumPy API 兼容数组库(Dask、CuPy、JAX、PyTorch、TensorFlow、cuNumeric 等)和编译器/转换器(Numba、Pythran、Cython 等)的维护人员来说,命名空间中的每个对象都会带来实现成本。实际上,没有其他库完全支持整个 NumPy API,部分原因是面对大量别名和遗留对象时,很难知道要包含哪些内容。

  • 教授 NumPy 比必要情况更加复杂。

    类似地,更大的 API 会让学习者感到困惑,他们不仅需要查找函数,还需要选择使用哪个函数。

  • 开发人员不愿增加 API 表面。

    即使更改是合理的,也会发生这种情况,因为他们意识到上述问题。

此 NEP 的范围包括

  • 弃用或删除对于 NumPy 来说过于小众、设计不佳、被更好的替代方案取代、不必要的别名或以其他方式适合删除的功能。

  • 通过使用下划线明确区分公共 NumPy API 和私有 NumPy API。

  • 重构 NumPy 命名空间,使其更易于理解和导航。

此 NEP 范围之外的是

  • 引入新功能或性能增强。

使用和影响#

此 API 重构的一个关键原则是确保,当代码已适应更改并与 2.0 兼容时,该代码也适用于 NumPy 1.2x.x。这通过不必携带根据 NumPy 主版本号切换的重复代码,从而降低了用户和下游库维护人员的负担。

向后兼容性#

如上所述,虽然新的(或已清理的,NumPy 2.0)API 应该向后兼容,但不能保证从 1.25.X 到 2.0 的向前兼容性。代码将需要更新以考虑已弃用、移动或删除的函数/类,以及更严格执行的私有 API。

为了更容易地采用此 NEP 中的更改,我们将

  1. 提供一个过渡指南,其中列出了每个 API 更改及其替换。

  2. 使用有意义的 AttributeError 明确标记所有已过期的属性,该错误会指出新位置或推荐替代方案。

  3. 提供一个脚本来尽可能地自动执行迁移。这类似于 tools/replace_old_macros.sed(它使代码适应以前的 C API 命名方案更改)。这将基于 sed(或等效项),而不是尝试 AST 分析,因此它不会涵盖所有内容。

详细描述#

清理主命名空间#

我们预计将减少主命名空间中的大量条目,大约 100 个。以下是一些有代表性的示例

  • np.infnp.nan 之间有 8 个别名,其中大部分可以删除。

  • gh-12385 中列出的一组随机且未记录的函数(例如,byte_boundsdispsafe_evalwho)可以被弃用并删除。

  • 所有 *sctype 函数都可以被弃用并删除,它们(请参阅 gh-17325gh-12334 和其他关于 maximum_sctype 和相关函数的问题)。

  • 在 Python 2 到 3 的过渡期间使用的 np.compat 命名空间将被删除。

  • 范围狭窄且公开使用案例很少的函数将被删除。这些将需要手动识别并通过问题分类进行识别。

为警告/异常(np.exceptions)和与数据类型相关的功能(np.dtypes)引入了新的命名空间。NumPy 2.0 是一个很好的机会,可以将这些子模块从主命名空间中填充。

广泛使用但有首选替代方案的功能,可以弃用(弃用消息指出应改用什么),也可以通过不将其包含在 __dir__ 中来隐藏。在隐藏的情况下,可以使用 .. legacy:: 目录在文档中标记此类功能。

将添加一个测试以确保所有命名空间的未来增长有限;也就是说,每个新条目都需要显式添加到允许列表中。

清理子模块结构#

我们将清理 NumPy 子模块结构,以便于导航。当之前讨论过这个问题时(参见 MAINT: Hide internals of np.lib to only show submodules),已经达成了大致的共识 - 但是很难在次要版本中实现。

我们将坚持的基本原则是“一个函数,一个位置”。在多个命名空间中公开的函数(例如,许多函数存在于 numpynumpy.lib 中)需要找到一个唯一的归宿。

我们将根据主命名空间和子模块命名空间重新组织 API 参考指南,并且仅在主命名空间中使用当前沿功能分组的细分。以及“主流”和特殊用途命名空间

# Regular/recommended user-facing namespaces for general use. Present these
# as the primary set of namespaces to the users.
numpy
numpy.exceptions
numpy.fft
numpy.linalg
numpy.polynomial
numpy.random
numpy.testing
numpy.typing

# Special-purpose namespaces. Keep these, but document them in a separate
# grouping in the reference guide and explain their purpose.
numpy.array_api
numpy.ctypeslib
numpy.emath
numpy.f2py  # only a couple of public functions, like `compile` and `get_include`
numpy.lib.stride_tricks
numpy.lib.npyio
numpy.rec
numpy.dtypes
numpy.array_utils

# Legacy (prefer not to use, there are better alternatives and/or this code
# is deprecated or isn't reliable). This will be a third grouping in the
# reference guide; it's still there, but de-emphasized and the problems
# with it or better alternatives are explained in the docs.
numpy.char
numpy.distutils
numpy.ma
numpy.matlib

# To remove
numpy.compat
numpy.core  # rename to _core
numpy.doc
numpy.math
numpy.version  # rename to _version
numpy.matrixlib

# To clean out or somehow deal with: everything in `numpy.lib`

注意

待定:我们将保留 np.lib 还是不保留?它只有几个独特的函数/对象,例如 Arrayterator(一个待移除的候选对象)、NumPyVersion 以及 stride_tricksmixinsformat 子子模块。numpy.lib 本身不是一个连贯的命名空间,甚至没有参考指南页面。

我们将使所有子模块都能够延迟加载,以便用户不必键入 import numpy.xxx,而是可以使用 import numpy as np; np.xxx.*,同时不会对 import numpy 的开销产生负面影响。这对教授 scikit-image 和 SciPy 非常有帮助,它解决了 Spyder 用户的一个潜在问题,因为 Spyder 已经使所有子模块都可用 - 因此使用上述导入模式的代码在 Spyder 中可以工作,但在外部却无法工作。

减少选择数据类型的方式#

众多数据类型类、实例、别名以及选择它们的方式是 NumPy API 中较大的可用性问题之一。例如

>>> # np.intp is different, but compares equal too
>>> np.int64 == np.int_ == np.dtype('i8') == np.sctypeDict['i8']
True
>>> np.float64 == np.double == np.float_ == np.dtype('f8') == np.sctypeDict['f8']
True
### Really?
>>> np.clongdouble == np.clongfloat == np.longcomplex == np.complex256
True

这些别名可以删除:https://numpy.com.cn/devdocs/reference/arrays.scalars.html#other-aliases

所有单字符类型代码字符串以及相关的例程(如 mintypecode)将被标记为旧版。

讨论

  • 所有与数据类型相关的类移动到 np.dtypes 中?

  • 比较/选择数据类型的规范方法:np.isdtype(新的,交叉引用数组 API NEP),保留 np.issubdtype 用于 NumPy 的数据类型类层次结构的更利基用途,并隐藏大多数其他内容。

  • 是否可能移除 float96/float128?它们是可能不存在的别名,并且很容易用它们误伤自己。

清理 numpy.ndarray 上的利基方法#

ndarray 对象有很多属性和方法,其中一些过于利基,不应那么突出,所有这些只会分散普通用户的注意力。例如

  • .itemset(已不鼓励使用)

  • .newbyteorder(过于利基)

  • .ptp(利基,改用 np.ptp 函数)

已考虑并拒绝的 API 更改#

对于某些函数和子模块,事实证明,删除它们会导致过多的中断,或者需要的工作量与实际收益不成比例。我们针对此类项目得出了这一结论

  • 移除工作日函数:np.busday_countnp.busday_offsetnp.busdaycalendar

  • 移除 np.nan* 函数并向相关的基本函数引入新的 nan_mode 参数。

  • 隐藏 np.histograms 子模块中的直方图函数。

  • 隐藏 np.lib.index_tricks 子模块中的 c_r_s_

  • 看起来很利基但存在于数组 API 中的函数(例如 np.can_cast)。

  • ndarray 对象中移除 .repeat.ctypes

实施#

实现已拆分为许多不同的 PR,每个 PR 都涉及单个 API 或一组相关的 API。以下是一些影响最大的 PR 的示例

可以通过搜索专用标签来找到 2.0 版本中完成的所有清理工作

一些 PR 已经合并并随 1.25.0 版本一起发布。例如,弃用非首选别名

隐藏或移除意外公开的对象或根本不是 NumPy 对象的对象

创建新的命名空间以更轻松地导航模块结构

替代方案#

讨论#

参考文献和脚注#