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 新增了一個 @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屬性。這也是 CPythonPy_DEPRECATED巨集的基礎。 - GraphQL 支援將欄位標記為
@deprecated。 - Kotlin 支援
Deprecated註解。 - Scala 支援
@deprecated註解。 - Swift 支援使用
@available屬性來標記 API 為棄用。 - TypeScript 使用
@deprecatedJSDoc 標籤來發出棄用功能的使用提示。
已有數名使用者請求支援此類功能
已有類似的第三方工具
- Deprecated 提供了一個裝飾器來標記類別、函式或方法為棄用。存取已標記的物件會引發執行時期警告,但不會被型別檢查器偵測到。
- flake8-deprecated 是一個 linter 外掛程式,會對棄用功能的用法發出警告。然而,它僅限於一個簡短且硬編碼的棄用列表。
規範
在 warnings 模組中新增了一個新裝飾器 @deprecated()。此裝飾器可用於類別、函式或方法,將其標記為棄用。這包含 typing.TypedDict 和 typing.NamedTuple 的定義。對於多載 (overloaded) 函式,裝飾器可以應用於個別的多載,表示該特定多載已被棄用。裝飾器也可以應用於多載的實作函式,表示整個函式已被棄用。
該裝飾器接受以下參數:
- 一個必填且僅限位置 (positional-only) 的參數,代表棄用訊息。
- 兩個僅限關鍵字 (keyword-only) 的參數:
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)- 任何間接觸發該函式呼叫的語法。例如,若類別
C的__add__方法被棄用,則程式碼C() + C()應觸發診斷。同理,若屬性的 setter 被標記為棄用,嘗試設定該屬性亦應觸發診斷。
若方法使用了來自 PEP 698 的 typing.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 387 及 DeprecationWarning 文件)應將此裝飾器提及為標記棄用功能的額外方式。
參考實作
@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。
對於模組層級常數、物件屬性及函式參數的棄用,可以新增類似 Annotated 的 Deprecated[type, message] 型別修飾符。然而,這會在型別系統中建立一個新的位置,使字串僅為字串而非前向參考 (forward reference),從而增加型別檢查器的實作複雜度。此外,我的數據顯示此功能並不常被需求。
未來可在其他 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