NEP 52 — Python API 清理(针对 NumPy 2.0)#

作者:

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/numpy-discussion@python.org/thread/QLMPFTWA67DXE3JCUQT2RIRLQ44INS4F/

摘要#

我们提议为 NumPy 2.0 版本清理 NumPy 的 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、Jupyter Notebook 或 IDE 中使用 Tab 键补全查找函数是一项挑战。例如,键入 np.<TAB> 并查看前六个提供的项目:两个 ufuncs (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.infnp.nan 之间有 8 个别名,其中大部分可以被移除。

  • gh-12385 中列出的一系列随机且未文档化的函数(例如,byte_boundsdispsafe_evalwho)可以被废弃和移除。

  • 所有 *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),对此已经有大致共识——但在次要版本中很难实现。

我们将遵循的基本原则是“一个函数,一个位置”。在多个命名空间中暴露的函数(例如,许多函数同时存在于 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 中可以工作,但在 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.net.cn/doc/stable/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_countnp.busday_offsetnp.busdaycalendar

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

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

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

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

  • ndarray 对象中移除 .repeat.ctypes

实现#

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

2.0 版本中已完成的清理工作的完整列表可以通过搜索专用标签找到

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

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

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

替代方案#

讨论#

参考文献与脚注#