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

Python 增強提案 (Python Enhancement Proposals)

PEP 702 – 使用型別系統標記棄用

作者:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
討論於:
Discourse 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
主題:
類型標註 (Typing)
建立日期:
2022年12月30日
Python 版本:
3.13
公告歷史:
2023年1月1日, 2023年1月22日
決議:
2023年11月7日

目錄

重要資訊

本 PEP 為歷史文件:請參閱 @deprecated@warnings.deprecated 以獲取最新的規範與說明文件。標準的型別規範維護於 typing specs site;執行時期的型別行為則描述於 CPython 文件中。

×

有關如何提議更改型別規格,請參閱 typing 規格更新流程

摘要

本 PEP 新增了一個 @warnings.deprecated() 裝飾器,用於將類別或函式標記為棄用,使靜態檢查器能在該物件被使用時發出警告。預設情況下,此裝飾器還會觸發執行時期的 DeprecationWarning

動機

隨著軟體演進,新功能會被新增,而舊功能則會變得過時。函式庫開發者希望在移除過時程式碼的同時,給予使用者足夠的時間遷移至新的 API。Python 提供了一種達成此目標的機制:DeprecationWarning 警告類別,用於在棄用功能被使用時顯示警告。此機制已被廣泛使用:截至撰寫本 PEP 時,CPython 主分支包含約 150 個會觸發 DeprecationWarning 的不同程式碼路徑。許多第三方函式庫也使用 DeprecationWarning 來標記棄用。在 前 5000 個 PyPI 套件 中,有:

  • 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 的定義。對於多載 (overloaded) 函式,裝飾器可以應用於個別的多載,表示該特定多載已被棄用。裝飾器也可以應用於多載的實作函式,表示整個函式已被棄用。

該裝飾器接受以下參數:

  • 一個必填且僅限位置 (positional-only) 的參數,代表棄用訊息。
  • 兩個僅限關鍵字 (keyword-only) 的參數:categorystacklevel,用於控制執行時期的行為(參見下方「執行時期行為」)。

僅限位置參數的型別為 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)
  • 任何間接觸發該函式呼叫的語法。例如,若類別 C__add__ 方法被棄用,則程式碼 C() + C() 應觸發診斷。同理,若屬性的 setter 被標記為棄用,嘗試設定該屬性亦應觸發診斷。

若方法使用了來自 PEP 698typing.override() 裝飾器,且其覆寫 (override) 的父類別方法已被棄用,則型別檢查器應產生診斷資訊。

還有其他棄用可能發揮作用的場景。例如,物件可能實作了 typing.Protocol,但符合該協定所需的其中一個方法已被棄用。由於這類場景相當複雜且在實務上相對罕見,本 PEP 不強制型別檢查器對其進行偵測。

範例

作為範例,考慮此名為 library.pyi 的函式庫 stub:

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:發出警告時要略過的堆疊幀 (stack frames) 數量。預設為 1,表示警告應在呼叫棄用物件的地點發出。在內部實作中,將會加上封裝程式碼所使用的堆疊幀數量。

若被裝飾物件為類別,裝飾器會封裝 __new__ 方法,使得實例化該類別時會發出警告。若被裝飾物件為可呼叫物件 (callable),裝飾器會傳回一個新的可呼叫物件,封裝原始物件並在呼叫時引發警告。否則,裝飾器會引發 TypeError(除非傳入了 category=None)。

有幾種情境下,使用已裝飾物件無法發出警告,包括多載、Protocol 類別及抽象方法。在這些情況下,若在未使用 category=None 的情況下使用 @deprecated,型別檢查器可能會顯示警告。

為了適應執行時期的自我檢測 (introspection),裝飾器會在傳入的物件上(以及為棄用類別與函式所產生的封裝可呼叫物件上)設定一個 __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 文件)應將此裝飾器提及為標記棄用功能的額外方式。

參考實作

@deprecated 裝飾器的執行時期實作已於 typing-extensions 函式庫 4.5.0 版本中提供。pyanalyze 型別檢查器已具備發出棄用錯誤的 原型支援Pyright 亦然。

遭否決的想法

模組與屬性的棄用

本 PEP 涵蓋了類別、函式與多載的棄用。這使得型別檢查器能偵測許多(但非全部)可能的棄用情況。為了評估額外功能是否值得開發,我 檢視了 CPython 標準函式庫中所有當前的棄用情況。

我發現:

  • 74 個函式、方法與類別的棄用(本 PEP 支援)
  • 28 個模組整體的棄用(主要歸因於 PEP 594
  • 9 個函式參數的棄用(本 PEP 透過裝飾多載提供支援)
  • 1 個常數的棄用
  • 38 個在型別系統中不易偵測的棄用(例如,在沒有活動事件迴圈的情況下呼叫 asyncio.get_event_loop()

可以透過新增 __deprecated__ 模組層級常數來將模組標記為棄用。然而,此需求有限,且透過 grep 即可相對容易地偵測對已棄用模組的使用。因此,本 PEP 省略了對模組整體棄用的支援。作為替代方案,使用者可以在所有模組層級的類別與函式上加上 @deprecated

對於模組層級常數、物件屬性及函式參數的棄用,可以新增類似 AnnotatedDeprecated[type, message] 型別修飾符。然而,這會在型別系統中建立一個新的位置,使字串僅為字串而非前向參考 (forward reference),從而增加型別檢查器的實作複雜度。此外,我的數據顯示此功能並不常被需求。

未來可在其他 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