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) 和与 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中有效,但在外部无效。

减少选择dtype的方法#

许多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

  • 比较/选择dtype的规范方法: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子模块中的直方图函数。

  • 隐藏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对象的对象

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

替代方案#

讨论#

参考文献和脚注#