NEP 23 — 向后兼容性与废弃策略#

作者

Ralf Gommers <ralf.gommers@gmail.com>

状态

活跃

类型

流程

创建日期

2018-07-14

决议

https://mail.python.org/pipermail/numpy-discussion/2021-January/081423.html

摘要#

在本NEP中,我们描述了NumPy的向后兼容性方法、其废弃和移除策略,以及在考虑破坏向后兼容性时,个别案例的权衡和决策过程。

动机和范围#

NumPy拥有庞大的用户群。这些用户依赖NumPy的稳定性以及他们使用NumPy功能编写的代码能持续工作。NumPy也积极维护和改进——有时改进需要或通过破坏向后兼容性来简化。最后,在现有用户的稳定性与避免错误或为新用户提供更好的用户体验之间存在权衡。这些相互竞争的需求经常导致长时间的争论,并延迟接受或拒绝贡献。本NEP试图通过提供策略以及何时破坏向后兼容性是或不是一个好主意的示例和理由来解决这个问题。

此外,本NEP可作为用户文档,说明NumPy项目如何处理向后兼容性,以及他们期望更改的速度。

本NEP的范围包括:

  • NumPy处理向后兼容性的原则。

  • 如何废弃功能,以及何时移除已废弃功能。

  • 废弃和移除的决策过程。

  • 如何确保用户充分了解任何更改。

超出范围的包括:

  • 对特定功能的废弃做出具体决定。

  • NumPy的版本控制方案。

一般原则#

在考虑向后不兼容的拟议更改时,NumPy开发者在做出决定时主要遵循的原则是:

  1. 更改带来的好处应大于其造成的损害。

  2. NumPy使用广泛,因此默认应认为破坏性更改是有害的。

  3. 决策应基于其对用户和下游包的影响,并应尽可能基于使用数据。这种使用是否与文档或最佳实践相悖并不重要。

  4. 出现错误结果比出现错误甚至崩溃更糟糕。

在评估拟议更改的成本时,请记住大多数用户不阅读邮件列表,不注意废弃警告,有时甚至等待一两年才从旧版本升级。而且NumPy有数百万用户,所以“没有人会这样做或使用这个”很可能是不正确的。

拟议更改的好处可能包括改进的功能、可用性和性能,以及降低维护成本和提高未来的可扩展性。

明确的错误修复不受本向后兼容性策略的约束。然而,如果对用户造成严重影响,即使是错误修复也可能需要推迟一个或多个版本。例如,如果某个下游库无法再构建或给出不正确的结果。

实施废弃和移除#

在所有最终将移除功能的案例中,废弃警告都是必要的。如果无意移除某功能,则不应将其废弃。此时应在文档中使用“请勿在新代码中使用此功能”或其他类型的警告,并且可以调整文档结构,以便更突出地显示首选替代方案。

废弃

  • 应包含该功能被废弃的发布版本号。

  • 应包含废弃功能的替代方案信息,如果没有明确替代方案,则应说明废弃原因。请注意,发布说明在需要时可以包含更长的消息。

  • 默认应使用 DeprecationWarning,对于已废弃但需要再次关注或因某种原因需要额外关注的更改,应使用 VisibleDeprecation

  • 应列在首次引入该废弃的发布版本的发布说明中。

  • 不得在微(错误修复)版本中引入。

  • 应设置 stacklevel,以便警告显示来自正确的位置。

  • 应在功能文档中提及。为此可以使用 .. deprecated:: 指令。

良好废弃警告的示例(另请注意警告上方注释的标准格式,有助于在 grep 时查找)

# NumPy 1.15.0, 2018-09-02
warnings.warn('np.asscalar(a) is deprecated since NumPy 1.16.0, use '
              'a.item() instead', DeprecationWarning, stacklevel=3)

# NumPy 1.15.0, 2018-02-10
warnings.warn("Importing from numpy.testing.utils is deprecated "
              "since 1.15.0, import from numpy.testing instead.",
              DeprecationWarning, stacklevel=2)

# NumPy 1.14.0, 2017-07-14
warnings.warn(
    "Reading unicode strings without specifying the encoding "
    "argument is deprecated since NumPy 1.14.0. Set the encoding, "
    "use None for the system default.",
    np.VisibleDeprecationWarning, stacklevel=2)
/* DEPRECATED 2020-05-13, NumPy 1.20 */
if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
        matrix_deprecation_msg, ufunc->name, "first") < 0) {
    return NULL;
}

废弃功能的移除

  • 假设当前的6个月发布周期,应在至少2个发布版本之后进行;如果发布周期改变,废弃和移除之间应至少间隔1年。

  • 应列在发生移除的发布版本的发布说明中。

  • 可以在任何次要版本中进行,但不能在错误修复版本中进行。

对于不属于“废弃并移除”但代码行为将开始发生变化的向后不兼容更改,应使用 FutureWarning。发布说明中应提及版本号并使用 stacklevel,方式与废弃警告相同。在行为更改后,文档中应使用 .. versionchanged:: 指令来指明行为何时发生变化。

def argsort(self, axis=np._NoValue, ...):
    """
    Parameters
    ----------
    axis : int, optional
        Axis along which to sort. If None, the default, the flattened array
        is used.

        ..  versionchanged:: 1.13.0
            Previously, the default was documented to be -1, but that was
            in error. At some future date, the default will change to -1, as
            originally intended.
            Until then, the axis should be given explicitly when
            ``arr.ndim > 1``, to avoid a FutureWarning.
    """
    ...
    warnings.warn(
        "In the future the default for argsort will be axis=-1, not the "
        "current None, to match its documentation and np.argsort. "
        "Explicitly pass -1 or None to silence this warning.",
        MaskedArrayFutureWarning, stacklevel=3)

决策制定#

在需要应用此策略的具体案例中,决策将根据NumPy治理模型进行。

所有废弃都必须在邮件列表中提出,以便所有对NumPy开发感兴趣的人有机会发表评论。废弃功能的移除不需要在邮件列表中讨论。

具有更严格废弃策略的功能#

  • numpy.random 有其自己的向后兼容性策略,除了本NEP中的要求外,还有额外要求,详见 NEP 19 — 随机数生成器策略

  • .npy.npz 文件的文件格式与NumPy版本严格独立地进行版本控制;即使引入了较新的格式版本,现有格式版本也必须保持向后兼容。

案例示例#

我们现在讨论NumPy历史中的一些具体案例,以说明典型问题和权衡。

更改函数行为

np.histogram 可能是最臭名昭著的例子。首先,引入了一个新关键字 new=False,一个版本后改为 None,最后又被移除。此外,它还有一个 normed 关键字,其行为可以被认为是次优的或有问题的(取决于对统计的看法)。引入了一个新关键字 density 来替代它;normed 直到v.1.15.0才开始发出 DeprecationWarninghistogram 的演变

def histogram(a, bins=10, range=None, normed=False):  # v1.0.0

def histogram(a, bins=10, range=None, normed=False, weights=None, new=False):  #v1.1.0

def histogram(a, bins=10, range=None, normed=False, weights=None, new=None):  #v1.2.0

def histogram(a, bins=10, range=None, normed=False, weights=None):  #v1.5.0

def histogram(a, bins=10, range=None, normed=False, weights=None, density=None):  #v1.6.0

def histogram(a, bins=10, range=None, normed=None, weights=None, density=None):  #v1.15.0
    # v1.15.0 was the first release where `normed` started emitting
    # DeprecationWarnings

``new`` 关键字从一开始就计划是临时的。这样的计划迫使用户多次更改其代码,这几乎从来都不是正确的做法。相反,更好的方法是废弃 ``histogram`` 并引入一个新函数 ``hist`` 来替代它。

禁止使用浮点数进行索引

使用浮点数对数组进行索引是要求模糊不清的事情,可能是用户代码中存在错误的迹象。经过一些讨论,废弃使用浮点数进行索引被认为是个好主意。这首先在v1.8.0版本中尝试,但在预发布测试中,很明显这将破坏许多依赖NumPy的库。因此,在发布之前将其恢复,以给这些库时间先修复其代码。它最终在v1.11.0中引入,并在v1.12.0中变为硬错误。

这一更改具有破坏性,但它确实捕获了SciPy和scikit-learn等库中的实际错误。总的来说,这一更改是值得的,并且首先在主分支中引入以允许测试,然后在发布前再次将其移除,是一种有用的策略。

看起来也是清理/改进的好例子的类似废弃

  • 移除废弃的布尔索引(2016年,参见 gh-8312

  • 废弃空数组的真值测试(2017年,参见 gh-9718

移除财务函数

财务函数(例如 np.pmt)名称简短且不具描述性,存在于NumPy主命名空间中,并且与NumPy的范围不完全吻合。它们在2008年经过邮件列表讨论后添加,当时意见分歧(但多数人赞成)。财务函数没有造成太多开销,但每年仍有多个关于它们的问题和PR,这花费了维护者的时间来处理。它们还使 numpy 命名空间变得混乱。关于移除它们的讨论在2013年(gh-2880,被拒绝)和2019年(NEP 32 — 从NumPy中移除财务函数,无重大异议地被接受)进行。

鉴于它们明显超出了NumPy的范围,在废弃期后将它们移至单独的 numpy-financial 包并从NumPy中移除是合理的。这还为用户提供了一个简单的更新代码方式,即执行 pip install numpy-financial

替代方案#

对废弃更积极。

更加积极的目标是让NumPy更快地发展。这将避免其他人(通常在多个地方)发明自己的解决方案,同时也有利于没有遗留代码库的用户。我们拒绝此替代方案,因为NumPy在科学Python生态系统中的地位——为了不将下游库和最终用户的额外维护负担增加到不可接受的水平,相当保守是必要的。

讨论#

参考文献和脚注#