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