Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 702 – 使用类型系统标记弃用

作者:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
主题:
类型标注
创建日期:
2022年12月30日
Python 版本:
3.13
发布历史:
2023年1月1日, 2023年1月22日
决议:
2023年11月7日

目录

重要

本 PEP 是一份历史文档:有关最新的规范和文档,请参阅@deprecated@warnings.deprecated。规范的类型规范在类型规范网站维护;运行时类型行为在 CPython 文档中描述。

×

有关如何提议更改类型规范的信息,请参阅类型规范更新过程

摘要

本 PEP 增加了一个 @warnings.deprecated() 装饰器,用于将类或函数标记为已弃用,使静态检查器能够在其被使用时发出警告。默认情况下,此装饰器还将在运行时引发 DeprecationWarning

动机

随着软件的发展,新的功能被添加,旧的功能变得过时。库开发人员希望致力于移除过时的代码,同时给用户时间迁移到新的 API。Python 提供了一种实现这些目标的机制:DeprecationWarning 警告类,用于在使用已弃用功能时显示警告。此机制被广泛使用:截至本 PEP 编写时,CPython 主分支包含大约 150 条不同的代码路径会引发 DeprecationWarning。许多第三方库也使用 DeprecationWarning 来标记弃用。在PyPI 排名前 5000 的软件包中,有

  • 1911 个匹配正则表达式 warnings\.warn.*\bDeprecationWarning\b,表示使用了 DeprecationWarning(不包括警告拆分为多行的情况);
  • 1661 个匹配正则表达式 ^\s*@deprecated,表示使用了某种弃用装饰器。

然而,目前的机制通常不足以确保已弃用功能的用户及时更新其代码。例如,移除各种长期弃用的 unittest 功能不得不回滚到 Python 3.11,以给用户更多时间更新代码。用户可能出于实际原因禁用警告来运行其测试套件,或者弃用可能在未被测试覆盖的代码路径中触发。

提供更多方法让用户了解已弃用功能可以加快迁移过程。本 PEP 建议利用静态类型检查器向用户传达弃用信息。此类检查器对用户代码有透彻的语义理解,使它们能够检测并报告单个 grep 调用无法找到的许多弃用。此外,许多类型检查器与 IDE 集成,使用户可以直接在编辑器中看到弃用警告。

基本原理

乍一看,弃用似乎不是类型检查器应该触及的话题。毕竟,类型检查器关注的是检查代码是否按原样工作,而不是潜在的未来更改。然而,类型检查器对代码执行的分析以查找类型错误与检测许多弃用所需的分析非常相似。因此,类型检查器非常适合查找和报告弃用。

其他语言已经有类似的功能

  • GCC 支持在函数声明上使用 deprecated 属性。这驱动了 CPython 的 Py_DEPRECATED 宏。
  • GraphQL 支持将字段标记为 @deprecated
  • Kotlin 支持 Deprecated 注解。
  • Scala 支持 @deprecated 注解。
  • Swift 支持使用 @available 属性将 API 标记为已弃用。
  • TypeScript 使用 @deprecated JSDoc 标签来发出提示,标记已弃用功能的使用。

几位用户已请求支持此类功能

存在类似的现有第三方工具

  • Deprecated 提供了一个装饰器,用于将类、函数或方法标记为已弃用。访问已装饰对象会引发运行时警告,但不会被类型检查器检测到。
  • flake8-deprecated 是一个 linter 插件,用于警告已弃用功能的使用。然而,它仅限于一个简短、硬编码的弃用列表。

规范

warnings 模块中添加了一个新的装饰器 @deprecated()。此装饰器可用于类、函数或方法,以将其标记为已弃用。这包括 typing.TypedDicttyping.NamedTuple 定义。对于重载函数,装饰器可以应用于单个重载,表示特定的重载已弃用。装饰器也可以应用于重载实现函数,表示整个函数已弃用。

装饰器接受以下参数

  • 一个必需的仅限位置参数,表示弃用消息。
  • 两个仅限关键字参数 categorystacklevel,用于控制运行时行为(参见下面的“运行时行为”)。

仅限位置参数的类型是 str,它包含在类型检查器遇到已装饰对象的用法时应显示的消。工具可以清理弃用消息以进行显示,例如使用 inspect.cleandoc() 或等效逻辑。消息必须是字符串文字。弃用消息的内容由用户决定,但可以包括已弃用对象将被移除的版本,以及有关建议的替换 API 的信息。

类型检查器在遇到标记为已弃用的对象的用法时应生成诊断。对于已弃用的重载,这包括所有解析为已弃用重载的调用。对于已弃用的类和函数,这包括

  • 通过模块、类或实例属性的引用(module.deprecated_objectmodule.SomeClass.deprecated_methodmodule.SomeClass().deprecated_method
  • 在其定义模块中对已弃用对象的任何使用(module.py 中的 x = deprecated_object()
  • 如果使用 import *,则使用模块中的已弃用对象(from module import *; x = deprecated_object()
  • from 导入(from module import deprecated_object
  • 任何间接触发函数调用的语法。例如,如果类 C__add__ 方法已弃用,则代码 C() + C() 应触发诊断。类似地,如果属性的 setter 被标记为已弃用,则尝试设置属性应触发诊断。

如果方法使用来自 PEP 698typing.override() 装饰器标记,并且它覆盖的基类方法已弃用,则类型检查器应生成诊断。

还有其他场景中可能涉及弃用。例如,一个对象可能实现一个 typing.Protocol,但协议合规性所需的方法之一已弃用。由于此类场景看起来复杂且在实践中相对不太可能出现,因此本 PEP 不强制类型检查器检测它们。

示例

例如,考虑这个名为 library.pyi 的库存根文件

from warnings import deprecated

@deprecated("Use Spam instead")
class Ham: ...

@deprecated("It is pining for the fiords")
def norwegian_blue(x: int) -> int: ...

@overload
@deprecated("Only str will be allowed")
def foo(x: int) -> str: ...
@overload
def foo(x: str) -> str: ...

class Spam:
    @deprecated("There is enough spam in the world")
    def __add__(self, other: object) -> object: ...

    @property
    @deprecated("All spam will be equally greasy")
    def greasy(self) -> float: ...

    @property
    def shape(self) -> str: ...
    @shape.setter
    @deprecated("Shapes are becoming immutable")
    def shape(self, value: str) -> None: ...

类型检查器应如何处理此库的使用

from library import Ham  # error: Use of deprecated class Ham. Use Spam instead.

import library

library.norwegian_blue(1)  # error: Use of deprecated function norwegian_blue. It is pining for the fiords.
map(library.norwegian_blue, [1, 2, 3])  # error: Use of deprecated function norwegian_blue. It is pining for the fiords.

library.foo(1)  # error: Use of deprecated overload for foo. Only str will be allowed.
library.foo("x")  # no error

ham = Ham()  # no error (already reported above)

spam = library.Spam()
spam + 1  # error: Use of deprecated method Spam.__add__. There is enough spam in the world.
spam.greasy  # error: Use of deprecated property Spam.greasy. All spam will be equally greasy.
spam.shape  # no error
spam.shape = "cube"  # error: Use of deprecated property setter Spam.shape. Shapes are becoming immutable.

诊断的具体措辞由类型检查器决定,不属于规范的一部分。

运行时行为

除了仅限位置的 message 参数外,@deprecated 装饰器还接受两个仅限关键字参数

  • category:一个警告类。默认为 DeprecationWarning。如果将其设置为 None,则在运行时不会发出警告,并且装饰器返回原始对象,但会设置 __deprecated__ 属性(见下文)。
  • stacklevel:发出警告时要跳过的堆栈帧数。默认为 1,表示警告应在调用已弃用对象的位置发出。在内部,实现将添加包装器代码中使用的堆栈帧数。

如果被装饰对象是一个类,则装饰器会包装 __new__ 方法,以便实例化该类时发出警告。如果被装饰对象是可调用对象,则装饰器返回一个新的可调用对象,该对象包装原始可调用对象,但在调用时会引发警告。否则,装饰器会引发 TypeError(除非传入 category=None)。

有几种情况,使用已装饰对象无法发出警告,包括重载、Protocol 类和抽象方法。在这些情况下,如果 @deprecated 在没有 category=None 的情况下使用,类型检查器可能会显示警告。

为了适应运行时自省,装饰器在其传入的对象上,以及为已弃用类和函数生成的包装器可调用对象上,设置一个属性 __deprecated__。属性的值是传递给装饰器的消息。不支持装饰不允许设置此属性的对象。

如果带有 @runtime_checkable 装饰器的 Protocol 被标记为已弃用,则 __deprecated__ 属性不应被视为协议的成员,因此它的存在不应影响 isinstance 检查。

为了与 typing.get_overloads() 兼容,@deprecated 装饰器应放置在 @overload 装饰器之后。

类型检查器行为

本 PEP 没有明确规定类型检查器应如何向其用户呈现弃用诊断。然而,一些用户(例如,仅针对特定 Python 版本的应用程序开发人员)可能不关心弃用,而另一些用户(例如,希望其库保持与未来 Python 版本兼容的库开发人员)则希望在其 CI 管道中捕获任何已弃用功能的使用。因此,建议类型检查器提供涵盖这两种用例的配置选项。与任何其他类型检查器错误一样,也可以使用 # type: ignore 注释忽略弃用。

弃用策略

我们建议更新 CPython 的弃用策略(PEP 387),要求新的弃用(如果可能)使用本 PEP 中的功能来提醒用户该弃用。具体来说,这意味着新的弃用应伴随着 typeshed 仓库的更改,以在适当位置添加 @deprecated 装饰器。此要求不适用于无法使用本 PEP 功能表达的弃用。

向后兼容性

创建新的装饰器不会带来向后兼容性问题。与所有新的类型功能一样,@deprecated 装饰器将添加到 typing_extensions 模块中,使其能够在旧版本的 Python 中使用。

如何教授

对于在 IDE 或类型检查器输出中遇到弃用警告的用户,他们收到的消息应该清晰明了。使用 @deprecated 装饰器将是一个高级功能,主要与库作者相关。装饰器应在相关文档中提及(例如,PEP 387DeprecationWarning 文档),作为标记已弃用功能的额外方式。

参考实现

typing-extensions 库自 4.5.0 版本起提供了 @deprecated 装饰器的运行时实现。pyanalyze 类型检查器提供了发出弃用错误的原型支持Pyright 也提供类似支持。

被拒绝的想法

模块和属性的弃用

本 PEP 涵盖类、函数和重载的弃用。这使得类型检查器能够检测到许多但并非所有可能的弃用。为了评估是否值得增加额外功能,我检查了 CPython 标准库中所有当前的弃用。

我发现了

  • 74 个函数、方法和类的弃用(本 PEP 支持)
  • 28 个整个模块的弃用(主要由于 PEP 594
  • 9 个函数参数的弃用(本 PEP 通过装饰重载支持)
  • 1 个常量的弃用
  • 38 个难以在类型系统中检测到的弃用(例如,在没有活动事件循环的情况下调用 asyncio.get_event_loop()

可以通过添加一个 __deprecated__ 模块级常量来标记模块已弃用。然而,这种需求有限,并且通过 grepping 可以相对容易地检测到已弃用模块的使用。因此,本 PEP 省略了对整个模块弃用的支持。作为一种变通方法,用户可以为所有模块级类和函数标记 @deprecated

对于模块级常量、对象属性和函数参数的弃用,可以添加一个类似于 AnnotatedDeprecated[type, message] 类型修饰符。然而,这将在类型系统中创建一个新的位置,其中字符串只是字符串,而不是前向引用,从而使类型检查器的实现复杂化。此外,我的数据显示,这种功能并不常用。

未来可以在其他 PEP 中添加弃用更多类型对象的功能。

将装饰器放置在 typing 模块中

本 PEP 的早期版本建议将 @deprecated 装饰器放置在 typing 模块中。然而,有反馈认为 typing 模块中的装饰器具有运行时行为是出乎意料的。因此,本 PEP 现在建议将装饰器添加到 warnings 模块中。

致谢

与 typing-sig 聚会小组的通话为该提案提供了有用的反馈。


来源:https://github.com/python/peps/blob/main/peps/pep-0702.rst

最后修改:2024-10-16 16:05:18 GMT