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 版本的 Python 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.` 并查看提供的第一个六个项目:两个 ufunc (`abs`、`add`)、一个别名 (`absolute`),以及三个不打算供最终用户使用的函数 (`add_docstring`、`add_newdoc`、`add_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 命名空间以使其更容易理解和导航。

本 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.inf` 和 `np.nan` 之间有 8 个别名,其中大多数可以删除。

  • gh-12385 中列出的一组随机且未记录的函数(例如,`byte_bounds`、`disp`、`safe_eval`、`who`)可以弃用并删除。

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

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

  • 范围狭窄、公共用例很少的函数将被删除。这些将需要手动识别和通过问题分类。

为警告/异常 (`np.exceptions`) 和与 dtype 相关功能 (`np.dtypes`) 引入了新的命名空间。NumPy 2.0 是从主命名空间填充这些子模块的好机会。

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

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

清理子模块结构#

我们将清理 NumPy 子模块结构,以便更容易导航。当之前讨论过这一点时(请参阅 MAINT: Hide internals of np.lib to only show submodules)时,对此已经达成粗略共识——但在次要版本中很难实现。

我们将坚持的基本原则是“一个函数,一个位置”。在多个命名空间中公开函数(例如,许多函数出现在 `numpy` 和 `numpy.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_tricks`、`mixins` 和 `format` 子子模块。`numpy.lib` 本身不是一个连贯的命名空间,甚至没有参考指南页面。

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

减少选择 dtypes 的方法数量#

许多 dtype 类、实例、别名以及选择它们的方法是 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`)将被标记为遗留。

要讨论

  • 所有 dtype 相关类移到 `np.dtypes` 中吗?

  • 比较/选择 dtypes 的规范方法:`np.isdtype`(新的,数组 API NEP),保留 `np.issubdtype` 用于 NumPy dtype 类层次结构的更小众用途,并隐藏大多数其他内容。

  • 可能移除 `float96` / `float128`?它们是可能不存在的别名,并且很容易造成脚踏陷阱。

清理 `numpy.ndarray` 上的小众方法#

`ndarray` 对象有很多属性和方法,其中一些方法太小众而无法那么突出,这只会分散普通用户的注意力。例如:

  • ` .itemset`(已不鼓励使用)

  • ` .newbyteorder`(太小众)

  • ` .ptp`(小众,请改用 `np.ptp` 函数)

考虑和拒绝的 API 更改#

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

  • 删除营业日函数:`np.busday_count`、`np.busday_offset`、`np.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 对象的物体

创建新的命名空间以方便导航模块结构

替代方案#

讨论#

参考资料和脚注#