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
- 决议:
摘要#
我们提议为 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 中的更改,我们将
提供一份过渡指南,列出每个 API 更改及其替代方案。
使用有意义的
AttributeError
显式标记所有已过期的属性,该错误将指示新位置或推荐替代方案。尽可能提供一个脚本来自动化迁移。这将类似于
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-17325、gh-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 中可以工作,但在 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_count
、np.busday_offset
、np.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 对象的对象
创建新命名空间,以便更轻松地导航模块结构
替代方案#
讨论#
参考文献与脚注#
版权#
本文档已置于公共领域。