弃用#

sktime 旨在对其用户保持稳定和可靠。我们的高层级策略以确保这一点是

sktime 永远不应在未提前至少一个(MINOR)版本周期给出清晰且可操作的警告的情况下破坏用户代码。”

这里,“破坏”明确包括抽象逻辑的改变,例如使用的算法,而不仅仅是导致异常或性能下降的改变。

例如,如果用户有代码

from sktime.forecasting.foo import BarForecaster

bar = BarForecaster(42, x=43)
bar.fit(y_train, fh=[1, 2, 3])
y_pred = bar.predict()

那么任何 sktime 版本都不应在未警告的情况下改变

  • BarForecaster 的导入位置

  • BarForecaster 的参数签名,包括参数的名称、顺序和默认值

  • BarForecaster 对给定参数执行的抽象算法

无需警告即可进行的更改

  • 在参数列表末尾添加更多参数,其默认值保留了先前的行为,前提是新参数有充分的文档说明

  • 纯粹的内部代码重构,只要公共 API 保持不变

  • 在不改变抽象算法的情况下改变实现,例如出于性能原因

本文档中概述的弃用策略提供了如何以用户友好和可靠的方式进行需要更改或弃用处理的更改的详细信息。

它附带了为开发人员准备的规范模式(含示例)以及供发布经理遵循的流程,以便策略易于遵守。

弃用策略#

sktime 版本发布 遵循 语义化版本控制。版本号表示 <major>.<minor>.<patch> 版本。

我们当前的弃用策略如下

  • 所有接口破坏性(非向下兼容)的公共接口更改必须伴随弃用。示例:现有参数默认值的更改、参数的移除。非示例:添加默认值导致先前行为的新参数。

  • 此类更改或移除仅发生在 MINOR 或 MAJOR 版本中,不会发生在 PATCH 版本中。

  • 弃用警告必须在更改或移除前至少包含在一个完整的 MINOR 版本周期内。因此,通常情况下,更改或移除发生在 第二个 后续 MINOR 版本发布时。

示例时间线

1. 开发者 A 决定在当前版本 v0.9.3 时,在不久的将来移除功能 X。

2. 因此,根据上述规定,我们应该引入一条弃用消息,从下一个版本(例如 v0.9.4)开始可见,该消息说明该功能将在 v0.11.0 时移除

3. 开发者 A 提交一个拉取请求(pull request)以移除功能 X,其中包含该弃用警告。该拉取请求由核心开发者审阅,开发者 A 的建议被接受或拒绝。

4. 如果在 v0.10.0 发布前被接受并合并,则该 PR 将包含在下一个版本中,并在发布说明中附带弃用通知。如果 PR 的接受延迟到 v0.10.0 之后但在 v0.11.0 之前,则计划的移除将推迟到 v0.12.0,且警告需要更新。

5. 开发者 A 为 v0.12.0 准备了另一个移除弃用警告和功能 X 的 PR,但尚未合并

6. 发布经理作为 v0.12.0 发布的一部分合并了第 5 部分中的 PR,从而实现了移除。v0.12.0 的发布说明中包含一条移除通知。

弃用与更改流程#

一般的弃用/更改流程包括两个部分

  • 由开发者安排弃用/更改

  • 由发布经理执行弃用/更改操作

开发者侧的流程发生在提出弃用的开发者所创建的 PR 中,具体如下

  • 发出警告。对于所有已弃用的功能,如果更改计划在未来两个 MINOR 版本周期内进行,我们将发出 DeprecationWarning 警告。否则,FutureWarning 也可接受。

  • 警告应具有指导性。警告消息应提供功能更改的版本号,描述新的用法以及下游代码中任何过渡性操作,并清晰说明预期更改的时间表(指定的版本)。

  • 文档字符串(Docstrings)应更新以反映弃用。 文档字符串应更新以反映弃用/更改。这通常包括弃用时间表、弃用前/后的功能。

  • 在代码中为发布经理添加 TODO 注释。 在所有应该移除或更改的代码片段中添加 TODO 注释,例如:TODO: remove in v0.11.0。TODO 注释应详细描述所有操作(例如移除参数、移除函数或代码块)。如果更改需要应用于多个地方,则放置多个 TODO 注释。确保 TODO 操作的结果经过测试,并且在由发布经理执行时不会导致测试中断。最好附带一个准备好的 PR,发布经理只需合并即可。

  • 与所有技术决策一样,弃用/更改首先在 PR 中提出,并需要由其他开发者审阅。

发布经理流程在每个版本发布时进行,具体如下

  • 在变更日志(changelog)中总结任何计划的弃用和更改。:一旦弃用/更改被计划,就应该在变更日志的“弃用和更改”部分宣布,提供确切的版本时间表,以及用户或第三方扩展维护者需要执行的任何操作(使用和扩展约定)。

  • 执行弃用和更改操作。 作为 MINOR 或 MAJOR 版本发布流程的一部分,发布经理会通过搜索 TODO 注释来查找所有计划移除的已弃用功能并进行移除。这些操作将按描述执行。如果操作导致 CI 失败,发布经理应开启一个议题并联系开发者迅速解决,如果这会过度延迟发布流程,则可能将该操作移至下一个发布周期。

  • 在变更日志(changelog)中总结任何已执行的弃用和更改。:所有已执行的弃用和更改都应该在变更日志的“弃用和更改”部分进行总结。

特殊弃用#

本节概述了一些高级情况下的弃用流程。

参数的弃用与更改#

以下是函数或类(例如,评估器)参数常见的弃用或更改情况

  • 更改参数的默认值

  • 重命名参数

  • 添加一个默认值改变先前行为的参数

  • 更改参数的顺序

  • 移除参数

在所有情况下,都需要确保

  • 在用户逻辑会发生变化的情况下发出警告

  • 警告消息包含如何更改代码以保留当前行为或更改为替代行为的完整方案

  • 给予充分通知,即警告消息在执行更改前至少存在一个 MINOR 版本周期

  • 为发布经理留下“todo”注释以执行更改,最好提供一个可供合并的更改分支/PR,以便在计划更改的版本时进行合并

如果用户的现有逻辑不会改变,则不需要此类警告,例如在以下情况下

  • 在参数列表末尾添加一个默认值保留先前行为的参数

  • 移除一个非默认值总是会引发意外异常的参数

上面各个情况的方案如下。

本文档的最后一部分“示例说明方案”提供了一些这些情况的完整示例。

更改参数的默认值#

要更改参数的默认值,请在实现更改的拉取请求中遵循步骤 1-3。

1. 在当前版本中,将默认值更改为 "changing_value"。内部,添加逻辑,如果参数设置为 "changing_value",则用旧的默认值覆盖参数的值。如果参数是评估器类的 __init__ 参数,则其值不能直接覆盖,但这需要在私有参数副本中完成,因为所有 __init__ 参数必须写入 self 而不改变。即,将参数不变地写入 self._<param_name>,并添加逻辑以旧默认值覆盖 self._<param_name> 的值,并确保在代码的其余部分使用 self._<param_name> 而不是 self.<param_name>

2. 如果参数以非默认值调用,则使用 sktime.utils.warnings.warn 添加警告。此警告应始终包含评估器/函数的名称、更改的版本,以及关于如何更改代码以保留先前行为的明确说明。例如,"参数 <param_name> sktime 版本 <version_number> 中, <estimator_name> 的默认值将从 <old_value> 更改为 <new_value> 。要保留先前行为,请显式将 <param_name> 设置为 <old_value>"

3. 在代码中添加 TODO 注释,以便在下一个 MINOR 版本周期中移除警告并更改默认值。例如,在定义参数的函数或类的顶部添加注释 # TODO <version_number>: <param_name> 的默认值更改为 <new_value>,更新文档字符串,并移除警告

4. 发布经理将在下一个 MINOR 版本周期中执行 TODO 操作,并移除 TODO 注释。最好提供一个发布经理可以直接合并的更改分支,并在 todo 中提及该 PR ID。

重命名参数#

要重命名参数,请在实现更改的拉取请求中遵循步骤 1-6。

1. 在当前版本中,在参数列表的末尾添加一个使用新名称的参数,其默认值与旧参数相同。不要移除旧参数。

2. 将旧参数的值更改为字符串 "deprecated"。将函数或类中所有使用旧参数的代码更改为使用新参数。这可以通过批量替换完成。

3. 在函数或类的 init 开始处,如果旧参数不是 "deprecated",则添加逻辑,用旧参数的值覆盖新参数的值。如果参数是评估器类的 __init__ 参数,则其值不能直接覆盖,但这需要在私有参数中完成,因为所有 __init__ 参数必须写入 self 而不改变。

4. 如果旧参数以非默认值调用,则使用 sktime.utils.warnings.warn 添加警告。此警告应始终包含评估器/函数的名称、更改的版本,以及关于如何更改代码以保留先前行为的明确说明。例如,"参数 <param_name> sktime 版本 <version_number> 中, <estimator_name> 的参数将从 <old_name> 重命名为 <new_name> 。要保留先前行为,请使用 <new_name> kwargs 调用而不是 <old_name>"

  1. 更新函数或类的文档字符串(docstring),使其仅引用新参数。

6. 在代码中添加 TODO 注释,以便在下一个 MINOR 版本周期中移除警告并更改默认值。例如,在定义参数的函数或类的顶部添加注释 # TODO <version_number>: 将参数 <old_name> 的名称更改为 <new_name>,移除末尾的旧参数,并移除警告

  1. 发布经理将在下一个 MINOR 版本周期中执行 TODO 操作,

并移除 TODO 注释。最好提供一个发布经理可以直接合并的更改分支,并在 todo 中提及该 PR ID。

添加一个默认值改变先前行为的参数#

这应该分两步完成

  • 添加参数,但其默认值保留了先前行为。由于这保留了先前行为,因此不需要弃用或更改机制。

  • 然后,遵循上面关于更改参数默认值的步骤。

更改参数的顺序#

应避免此类更改,因为其难以执行。如果可以使用上述任一更改模式替代,则更可取。

要更改参数的顺序,请在实现更改的拉取请求中遵循步骤 1-6。

1. 在当前版本中,将位置将发生改变的第一个参数及之后所有参数的默认值更改为 "position_change"

2. 内部,添加逻辑,如果参数设置为 "position_change",则用旧的默认值覆盖参数的值。对于评估器类的 __init__ 参数,其值不能直接覆盖,但这需要在私有参数副本中完成,因为所有 __init__ 参数必须写入 self 而不改变。即,将参数不变地写入 self._<param_name>,并添加逻辑以旧默认值覆盖 self._<param_name> 的值,并确保在代码的其余部分使用 self._<param_name> 而不是 self.<param_name>

3. 如果任何位置发生改变的参数以非默认值调用,则使用 sktime.utils.warnings.warn 添加警告。此警告应始终包含评估器/函数的名称、更改的版本,以及关于如何更改代码以保留先前行为的明确说明。说明应引导用户对所有位置发生改变的参数使用 kwargs 调用而不是位置调用。

4. 在代码中添加 TODO 注释,以便在下一个 MINOR 版本周期中移除警告并更改顺序,同时将默认值更改回旧的默认值。TODO 注释应包含完整的代码行。最好提供一个发布经理可以直接合并的更改分支,并在 todo 中提及该 PR ID。

移除参数#

如果要移除的参数不在参数列表的末尾位置,应首先将其移至参数列表的末尾。

对于参数的移除,遵循“更改默认值”的步骤,但警告消息不同,即参数将被移除。

错误消息应包含关于是否可以保留先前行为的详细信息,如果可以,说明在哪些情况下可以,以及如何做到。

弃用标签(tags)#

要弃用标签(tags),需要确保在使用该标签时发出警告。有两种常见情况:移除标签或重命名标签。

对于任一情况,可以使用辅助类 TagAliaserMixin(在 sktime.base 中)。

要弃用标签,请将 TagAliaserMixin 添加到 BaseEstimator 或其他 BaseObject 的子类。建议选择完全覆盖已弃用标签使用的最新子类。TagAliaserMixin 覆盖了标签相关方法,因此应是第一个继承的类(或者在多个混入类的情况下,早于 BaseObject)。

TagAliaserMixin 中的 alias_dict 包含一个已弃用标签的字典:对于移除,添加一个条目 "old_tag_name": ""。对于重命名,添加一个条目 "old_tag_name": "new_tag_name"deprecate_dict 包含重命名或移除的版本号,并且应与 alias_dict 具有相同的键。

TagAliaserMixin 类将确保在弃用期间,新标签别名旧标签,反之亦然。每当访问已弃用标签时,将发出信息性警告。

在弃用期后移除/重命名标签时,确保从 TagAliaserMixin 类中的字典中移除已移除的标签。如果不再有任何标签被弃用(例如,所有已弃用标签都被移除/重命名),确保移除此类作为 BaseObjectBaseEstimator 的父类。

示例说明方案#

下面是上述某些情况的示例模板。这些示例是针对具有 fit / predict 方法的类进行的,但同样的原则适用于函数或具有其他 API 的类。

更改参数的默认值#

更改前的代码#

class EstimatorName:
    """The old docstring.

    Parameters
    ----------
    parameter : str, default="old_default"
        The parameter description.
    """
    def __init__(self, parameter="old_default"):
        self.parameter = parameter

    def fit(self, X, y):
        parameter = self.parameter
        # Fit the model using parameter
        fitting_logic(parameter)
        return self

    def predict(self, X):
        parameter = self.parameter
        # Predict using the fitted model
        y_pred = prediction_logic(parameter)
        return y_pred

步骤 1:弃用期间#

此步骤由开发者在 PR 中完成。可选地,开发者可以为步骤 2 准备一个 PR,发布经理可以将其合并。

from sktime.utils.warnings import warn

# TODO (release <MAJOR>.<MINOR>.0)
# change the default of 'parameter' to <new_value>
# update the docstring for parameter
class EstimatorName:
    """The old docstring with deprecation info.

    Parameters
    ----------
    parameter : str, default="old_default"
        The parameter description.
        Default value of parameter will change to <new_value>
        in version '<MAJOR>.<MINOR>.0'.
    """
    def __init__(self, parameter="changing_value"):
        self.parameter = parameter
        # TODO (release <MAJOR>.<MINOR>.0)
        # change the default of 'parameter' to <new_value>
        # remove the following 'if' check
        # de-indent the following 'else' check
        if parameter == "changing_value":
            warn(
                "in `EstimatorName`, the default value of parameter 'parameter'"
                " will change to <new_value> in version '<MAJOR>.<MINOR>.0'. "
                "To keep current behaviour and to silence this warning, "
                "set 'parameter' to 'old' explicitly.",
                category=DeprecationWarning,
                obj=self,
            )
            self._parameter = "old_default"
        else:
            self._parameter = parameter

    def fit(self, X, y):
        parameter = self._parameter
        # Fit the model using parameter
        fitting_logic(parameter)
        return self

    def predict(self, X):
        parameter = self._parameter
        # Predict using the fitted model
        y_pred = prediction_logic(parameter)
        return y_pred

步骤 2:弃用期后#

此步骤由发布经理完成,可以通过合并准备好的 PR 或执行 TODO 操作来完成。

class EstimatorName:
    """The final docstring.

    Parameters
    ----------
    parameter : str, default="new_default"
        The parameter description.
    """
    def __init__(self, parameter="new_default"):
        self.parameter = parameter
        self._parameter = parameter

    def fit(self, X, y):
        parameter = self._parameter
        # Fit the model using parameter
        fitting_logic(parameter)
        return self

    def predict(self, X):
        parameter = self._parameter
        # Predict using the fitted model
        y_pred = prediction_logic(parameter)
        return y_pred

可选地,如果私有参数 self._parameter 在代码的其他地方未使用,则可以将其移除,并替换为 self.parameter

重命名参数#

更改前的代码#

class EstimatorName:
    """The old docstring.

    Parameters
    ----------
    old_parameter : str, default="default"
        The parameter description.
    """

    def __init__(self, old_parameter="default"):
        self.old_parameter = old_parameter

    def fit(self, X, y):
        old_parameter = self.old_parameter
        # Fit the model using parameter
        fitting_logic(old_parameter)
        return self

    def predict(self, X):
        old_parameter = self.old_parameter
        # Predict using the fitted model
        y_pred = prediction_logic(old_parameter)
        return y_pred

步骤 1:弃用期间#

此步骤由开发者在 PR 中完成。可选地,开发者可以为步骤 2 准备一个 PR,发布经理可以将其合并。

from sktime.utils.warnings import warn

 class EstimatorName:
     """The old docstring, but already points to the new name.

     The docstring should replace 'old_parameter' with 'new_parameter',
     and no longer mention 'old_parameter'.

     Parameters
     ----------
     new_parameter : str, default="default"
         The parameter description.
     """
     def __init__(self, old_parameter="deprecated", new_parameter="default"):
         # IMPORTANT: both params need to be written to self during change period
         self.new_parameter = new_parameter
         self.old_parameter = old_parameter
         # TODO (release <MAJOR>.<MINOR>.0)
         # remove the 'old_parameter' argument from '__init__' signature
         # move 'new_parameter' to the position of 'old_parameter'
         # remove the following 'if' check
         # de-indent the following 'else' check
         if old_parameter != "deprecated":
             warn(
                 "in `EstimatorName`, parameter 'old_parameter'"
                 " will be renamed to new_parameter in version '<MAJOR>.<MINOR>.0'. "
                 "To keep current behaviour and to silence this warning, "
                 "use 'new_parameter' instead of 'old_parameter', "
                 "set new_parameter explicitly via kwarg, and do not set"
                 " old_parameter.",
                 category=DeprecationWarning,
                 obj=self,
             )
             self._parameter = old_parameter
         else:
             self._parameter = new_parameter

    def fit(self, X, y):
         old_parameter = self._parameter
         # Fit the model using parameter
         fitting_logic(old_parameter)
         return self

    def predict(self, X):
         old_parameter = self._parameter
         # Predict using the fitted model
         y_pred = prediction_logic(old_parameter)
         return y_pred

步骤 2:弃用期后#

此步骤由发布经理完成,可以通过合并准备好的 PR 或执行 TODO 操作来完成。

class EstimatorName:
    """Same as in step 2, no change necessary.

    Parameters
    ----------
    new_parameter : str, default="default"
        The parameter description.
    """
   def __init__(self, new_parameter="default"):
       self.new_parameter = new_parameter
       self._parameter = new_parameter

   def fit(self, X, y):
        old_parameter = self._parameter
        # Fit the model using parameter
        fitting_logic(old_parameter)
        return self

   def predict(self, X):
        old_parameter = self._parameter
        # Predict using the fitted model
        y_pred = prediction_logic(old_parameter)
        return y_pred

可选地,如果私有参数 self._parameter 在代码的其他地方未使用,则可以将其移除,并替换为 self.new_parameter