PEP 702 – 使用类型系统标记弃用
- 作者:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终
- 类型:
- 标准跟踪
- 主题:
- 类型
- 创建时间:
- 2022-12-30
- Python 版本:
- 3.13
- 历史记录:
- 2023-01-01, 2023-01-22
- 决议:
- Discourse 消息
摘要
此 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 插件,用于警告使用已弃用的功能。但是,它仅限于一小部分硬编码的弃用列表。
规范
将一个新的装饰器 @deprecated()
添加到 warnings
模块中。此装饰器可用于类、函数或方法,以将其标记为已弃用。这包括 typing.TypedDict
和 typing.NamedTuple
定义。对于重载函数,装饰器可以应用于单个重载,表示该特定重载已弃用。装饰器也可以应用于重载实现函数,表示整个函数已弃用。
装饰器接受以下参数
- 一个必需的位置专用参数,表示弃用消息。
- 两个关键字专用参数,
category
和stacklevel
,控制运行时行为(见下文“运行时行为”)。
位置专用参数的类型为 str
,包含一个消息,当类型检查器遇到已装饰对象的用法时,应该显示该消息。工具可以清理弃用消息以供显示,例如使用 inspect.cleandoc()
或等效逻辑。消息必须是字符串字面量。弃用消息的内容由用户决定,但可能包括要移除弃用对象的版本,以及有关建议替换 API 的信息。
类型检查器应该在遇到标记为已弃用对象的用法时生成诊断信息。对于已弃用的重载,这包括所有解析为已弃用重载的调用。对于已弃用的类和函数,这包括
- 通过模块、类或实例属性的引用(
module.deprecated_object
、module.SomeClass.deprecated_method
、module.SomeClass().deprecated_method
) - 其定义模块中对已弃用对象的任何用法(在
module.py
中的x = deprecated_object()
) - 如果使用了
import *
,则来自模块的已弃用对象的用法(from module import *; x = deprecated_object()
) from
导入(from module import deprecated_object
)- 任何间接触发对该函数调用的语法。例如,如果类的
__add__
方法C
已弃用,那么代码C() + C()
应该触发诊断信息。类似地,如果属性的 setter 被标记为已弃用,则尝试设置属性应该触发诊断信息。
如果一个方法使用来自 PEP 698 的 typing.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 387 和 DeprecationWarning
文档)中提及,作为标记已弃用功能的另一种方法。
参考实现
自 4.5.0 版本起,@deprecated
装饰器的运行时实现已在 typing-extensions 库中提供。 pyanalyze
类型检查器对发出弃用错误具有 原型支持,Pyright 也是如此。
被拒绝的想法
模块和属性的弃用
此 PEP 涵盖了类、函数和重载的弃用。这使类型检查器能够检测大多数,但不是所有可能的弃用。为了评估是否值得添加其他功能,我 检查了 CPython 标准库中的所有当前弃用。
我发现
- 74 个函数、方法和类的弃用(此 PEP 支持)
- 28 个整个模块的弃用(主要是因为 PEP 594)
- 9 个函数参数的弃用(通过装饰重载,此 PEP 支持)
- 1 个常量的弃用
- 38 个在类型系统中难以检测的弃用(例如,在没有活动事件循环的情况下调用
asyncio.get_event_loop()
)
可以通过添加 __deprecated__
模块级常量来标记模块已弃用。但是,对它的需求有限,而且检测已弃用模块的使用相对容易,只需进行 grep 即可。因此,此 PEP 省略了对整个模块弃用的支持。作为解决方法,用户可以将所有模块级类和函数标记为 @deprecated
。
对于弃用模块级常量、对象属性和函数参数,可以添加类似于 Annotated
的 Deprecated[type, message]
类型修饰符。但是,这将在类型系统中创建一个新的位置,字符串只是字符串,而不是前向引用,这会使类型检查器的实现变得复杂。此外,我的数据表明此功能并不常见。
将来可以在 PEP 中添加弃用更多类型对象的特性。
将装饰器放在 typing
模块中
此 PEP 的早期版本建议将 @deprecated
装饰器放在 typing
模块中。但是,有反馈说,typing
模块中的装饰器具有运行时行为是不寻常的。因此,此 PEP 现在建议将装饰器添加到 warnings
模块中。
致谢
与 typing-sig 会议小组的电话会议带来了对该提议的宝贵反馈。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以较宽松的许可证为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0702.rst
最后修改时间:2024-09-03 16:59:36 GMT