致下游包作者#

本文档旨在解释编写依赖于 NumPy 的包的一些最佳实践。

理解 NumPy 的版本控制和 API/ABI 稳定性#

NumPy 使用标准的、PEP 440 兼容的版本控制方案:主版本号.次版本号.修订号主版本发布极不寻常,如果发生,很可能意味着 ABI 不兼容。NumPy 1.xx 版本发布于 2006 年至 2023 年;2024 年初发布的 NumPy 2.0 是第一个改变 ABI 的版本(次要版本中可能发生过几次针对边缘情况的轻微 ABI 不兼容)。次要版本定期发布,通常每 6 个月一次。次要版本包含新功能、弃用和移除以前弃用的代码。错误修复版本发布更频繁;它们不包含任何新功能或弃用。

重要的是要知道 NumPy,像 Python 本身和大多数其他知名的科学 Python 项目一样,**不**使用语义版本控制。相反,不向后兼容的 API 更改需要至少两个版本的弃用警告。欲了解更多详情,请参阅 NEP 23 — 向后兼容性和弃用策略

NumPy 既有 Python API 也有 C API。C API 可以直接使用,也可以通过 Cython、f2py 或其他此类工具使用。如果你的包使用 C API,那么 NumPy 的 ABI(应用程序二进制接口)稳定性很重要。NumPy 的 ABI 是向前兼容但不是向后兼容的。这意味着:针对 NumPy C API 的给定目标版本编译的二进制文件,在新版 NumPy 下仍能正确运行,但不能在旧版本下运行。

模块也可以在 CPython 的 abi3 模式 下安全地针对 NumPy 2.0 或更高版本构建,这允许针对单个(最低支持的)Python 版本进行构建,但与同一系列(例如 3.x)中的更高版本向前兼容。这可以大大减少需要构建和分发的 wheel 数量。欲了解更多信息和示例,请参阅 cibuildwheel 文档

针对 NumPy 主分支或预发布版本进行测试#

对于依赖 NumPy 的大型、积极维护的包,我们建议在 CI 中针对 NumPy 的开发版本进行测试。为了方便,每晚构建版本以 wheel 形式提供在 https://anaconda.org/scientific-python-nightly-wheels/。安装命令示例:

pip install -U --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy

这有助于在 NumPy 下一个版本发布之前检测并修复 NumPy 中的回归问题。此外,我们建议在此 CI 任务中将警告视为错误,无论是所有警告,还是至少是 DeprecationWarningFutureWarning。这会让你提早发现 NumPy 的变化,以便调整你的代码。

如果你想针对最新的 NumPy 每晚构建版本测试你自己的 wheel 构建,并且你正在使用 cibuildwheel,你可能需要在你的 CI 配置文件中添加类似以下内容:

CIBW_ENVIRONMENT: "PIP_PRE=1 PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple"

添加对 NumPy 的依赖#

构建时依赖#

注意

在 NumPy 1.25 之前,NumPy C-API 默认不以向后兼容的方式公开。这意味着当使用早于 1.25 的 NumPy 版本进行编译时,你必须使用你希望支持的最旧版本进行编译。这可以通过使用 oldest-supported-numpy 来实现。请参阅 NumPy 1.24 文档

如果一个包直接使用 NumPy C API,或者使用依赖于它的其他工具,如 Cython 或 Pythran,那么 NumPy 是该包的构建时依赖。

默认情况下,NumPy 将公开一个 API,该 API 向后兼容支持当前最旧兼容 Python 版本的 NumPy 版本。NumPy 1.25.0 支持 Python 3.9 及更高版本,而 NumPy 1.19 是第一个支持 Python 3.9 的版本。因此,我们保证在默认情况下,NumPy 1.25 将公开一个与 NumPy 1.19 兼容的 C-API。(具体版本在 NumPy 内部头文件中设置)。

NumPy 对所有次要版本也向前兼容,但主要版本将需要重新编译(请参阅下方的 NumPy 2.0 特定建议)。

默认行为可以通过添加以下内容进行自定义,例如:

#define NPY_TARGET_VERSION NPY_1_22_API_VERSION

在需要 NumPy C-API 的每个扩展模块中,包含任何 NumPy 头文件(或等效的 -D 编译器标志)之前。这主要在你需要使用新添加的 API 而不兼容旧版本时有用。

如果出于某种原因,你希望默认编译针对当前安装的 NumPy 版本,你可以添加:

#ifndef NPY_TARGET_VERSION
    #define NPY_TARGET_VERSION NPY_API_VERSION
#endif

这允许用户通过 -DNPY_TARGET_VERSION 覆盖默认设置。这个宏定义必须对每个扩展模块(使用 import_array())保持一致,并且也适用于 umath 模块。

当你针对 NumPy 进行编译时,你应该将适当的版本限制添加到你的 pyproject.toml 文件中(请参阅 PEP 517)。因为你的扩展将不兼容 NumPy 的新主要版本,并且可能不兼容非常旧的版本。

对于 conda-forge 包,请参阅此处

截至目前,通常只需包含:

host:
  - numpy
run:
  - {{ pin_compatible('numpy') }}

运行时依赖和版本范围#

NumPy 本身以及许多核心科学 Python 包已就放弃对旧版 Python 和 NumPy 的支持达成一致:NEP29。我们建议所有依赖 NumPy 的包遵循 NEP 29 中的建议。

对于运行时依赖,请在 setup.py 中使用 install_requires 指定版本范围(假设你使用 numpy.distutilssetuptools 进行构建)。

大多数依赖 NumPy 的库不需要设置版本上限:NumPy 会小心地保持向后兼容性。

话虽如此,如果你是 (a) 保证频繁发布的项目,(b) 使用了 NumPy API 表面的大部分,并且 (c) 担心 NumPy 的变化可能会破坏你的代码,你可以设置一个版本上限为 <主版本号.次版本号 + N,其中 N 不小于 3,主版本号.次版本号 是 NumPy 的当前版本 [*]。如果你使用 NumPy C API(直接或通过 Cython),你还可以固定当前主版本以防止 ABI 损坏。请注意,设置 NumPy 的版本上限可能会影响你的库与其他新包同时安装的能力

注意

SciPy 有更多关于如何构建 wheels 以及如何处理其构建时和运行时依赖的文档,请参阅此处

NumPy 和 SciPy 的 wheel 构建 CI 也可以作为参考,NumPy 的可以在此处找到,SciPy 的可以在此处找到。

NumPy 2.0 特定建议#

NumPy 2.0 是一个 ABI 不兼容版本,但它确实包含构建同时适用于 2.0 和 1.xx 版本的 wheel 的支持。重要的是要理解:

  1. 当你在构建时使用 NumPy 1.xx 版本为你的包构建 wheel 时,这些 wheel 将**无法与 NumPy 2.0 兼容**。

  2. 当你在构建时使用 NumPy 2.x 版本为你的包构建 wheel 时,这些 wheel 将**可以与 NumPy 1.xx 兼容**。

NumPy 2.0 ABI 首次保证稳定性的时间将是 2.0 的第一个发布候选版本 (即 2.0.0rc1) 发布之时。我们处理你对 NumPy 依赖的建议如下:

  1. 在你的包的主(开发)分支中,不要添加任何约束。

  2. 如果你依赖 NumPy C API(例如通过在 C/C++ 中直接使用,或通过使用 NumPy 的 Cython 代码),在你的包的发布版本/发布分支的依赖元数据中添加 numpy<2.0 的要求。这样做直到 NumPy 2.0.0rc1 发布并且你可以将其作为目标版本。理由:NumPy C ABI 在 2.0 中将发生变化,因此任何依赖 NumPy 的已编译扩展模块都将损坏;它们需要重新编译。

  3. 如果你依赖 NumPy Python API 的大部分 API 表面,也考虑将相同的 numpy<2.0 要求添加到你的元数据中,直到你确定你的代码已针对 2.0 中的更改进行了更新(即,当你测试了代码在 2.0.0rc1 下正常工作时)。理由:我们将进行一次重要的 API 清理,许多别名和已弃用/不推荐的对象将被移除(例如,请参阅 NumPy 2.0 迁移指南 NEP 52 — NumPy 2.0 的 Python API 清理),所以除非你只使用现代/推荐的函数和对象,否则你的代码很可能需要至少进行一些调整。

  4. 计划在 NumPy 2.0 的第一个发布候选版本发布后不久(可能在 2024 年 2 月 1 日左右)发布你自己的依赖于 numpy 的包。理由:届时,你可以发布同时适用于 2.0 和 1.X 的包,因此你的最终用户不会看到太多/任何中断(你希望在 NumPy 2.0 发布当天 pip install mypackage 仍然能够正常工作)。

  5. 一旦 2.0.0rc1 可用,你可以按照下面概述的方式调整 pyproject.toml 中的元数据。

有两种情况:你需要保持与 NumPy 1.xx 的兼容性同时支持 2.0,或者你可以放弃对包新版本的 NumPy 1.xx 支持并仅支持 >=2.0。后者更简单,但可能对你的用户限制更多。在这种情况下,只需将 numpy>=2.0(或 numpy>=2.0.0rc1)添加到你的构建和运行时要求中,即可开始。我们现在将重点讨论“保持与 1.xx 和 2.x 兼容”的情况,这稍微复杂一些。

使用 NumPy C API(通过 C/Cython/等)并希望支持 NumPy 1.23.5 及更高版本的包示例:

[build-system]
build-backend = ...
requires = [
    # Note for packagers: this constraint is specific to wheels
    # for PyPI; it is also supported to build against 1.xx still.
    # If you do so, please ensure to include a `numpy<2.0`
    # runtime requirement for those binary packages.
    "numpy>=2.0.0rc1",
    ...
]

[project]
dependencies = [
    "numpy>=1.23.5",
]

我们建议你至少有一个 CI 任务,通过 wheel 构建/安装,然后针对包支持的最旧 NumPy 版本运行测试。例如:

- name: Build wheel via wheel, then install it
  run: |
    python -m build  # This will pull in numpy 2.0 in an isolated env
    python -m pip install dist/*.whl

- name: Test against oldest supported numpy version
  run: |
    python -m pip install numpy==1.23.5
    # now run test suite

上述方法仅在 NumPy 2.0 在 PyPI 上可用时才有效。如果你想针对 NumPy 2.0-dev 的 wheel 进行测试,你必须使用 NumPy 每晚构建版本(请参阅本节,更上方)或从源代码构建 NumPy。