註釋 (annotation) 最佳實踐¶
- 作者:
Larry Hastings
在 Python 3.10 及更高版本中存取物件的註釋字典¶
Python 3.10 在標準函式庫中新增了一個新函式:inspect.get_annotations()。在 Python 3.10 到 3.13,呼叫此函式是存取任何支援註釋的物件的註釋字典的最佳實踐。此函式也可以為你「取消字串化 (un-stringize)」字串化註釋。
In Python 3.14, there is a new annotationlib module
with functionality for working with annotations. This
includes a annotationlib.get_annotations() function,
which supersedes inspect.get_annotations().
若由於某種原因 inspect.get_annotations() 對你的場合不可行,你可以手動存取 __annotations__ 資料成員。 Python 3.10 中的最佳實踐也已經改變:從 Python 3.10 開始,保證 o.__annotations__ 始終適用於 Python 函式、類別 (class) 和模組。如果你確定正在檢查的物件是這三個特定物件之一,你可以簡單地使用 o.__annotations__ 來取得物件的註釋字典。
但是,其他型別的 callable(可呼叫物件)(例如,由 functools.partial() 建立的 callable)可能沒有定義 __annotations__ 屬性 (attribute)。當存取可能未知的物件的 __annotations__ 時,Python 3.10 及更高版本中的最佳實踐是使用三個參數呼叫 getattr(),例如 getattr(o, '__annotations__', None)。
在 Python 3.10 之前,存取未定義註釋但具有註釋的父類別的類別上的 __annotations__ 將傳回父類別的 __annotations__。在 Python 3.10 及更高版本中,子類別的註釋將會是一個空字典。
在 Python 3.9 及更早版本中存取物件的註釋字典¶
在 Python 3.9 及更早版本中,存取物件的註釋字典比新版本複雜得多。問題出在於這些舊版 Python 中有設計缺陷,特別是與類別註釋有關的設計缺陷。
存取其他物件(如函式、其他 callable 和模組)的註釋字典的最佳實踐與 3.10 的最佳實踐相同,假設你沒有呼叫 inspect.get_annotations():你應該使用三個:參數 getattr() 來存取物件的 __annotations__ 屬性。
不幸的是,這不是類別的最佳實踐。問題是,由於 __annotations__ 在類別上是選填的 (optional),並且因為類別可以從其基底類別 (base class) 繼承屬性,所以存取類別的 __annotations__ 屬性可能會無意中回傳基底類別的註釋字典。舉例來說:
class Base:
a: int = 3
b: str = 'abc'
class Derived(Base):
pass
print(Derived.__annotations__)
這將印出 (print) 來自 Base 的註釋字典,而不是 Derived。
如果你正在檢查的物件是一個類別 (isinstance(o, type)),你的程式碼將必須有一個單獨的程式碼路徑。在這種情況下,最佳實踐依賴 Python 3.9 及之前版本的實作細節 (implementation detail):如果一個類別定義了註釋,它們將儲存在該類別的 __dict__ 字典中。由於類別可能定義了註釋,也可能沒有定義,因此最佳實踐是在類別字典上呼叫 get() 方法。
總而言之,以下是一些範例程式碼,可以安全地存取 Python 3.9 及先前版本中任意物件上的 __annotations__ 屬性:
if isinstance(o, type):
ann = o.__dict__.get('__annotations__', None)
else:
ann = getattr(o, '__annotations__', None)
運行此程式碼後,ann 應該是字典或 None。我們鼓勵你在進一步檢查之前使用 isinstance() 仔細檢查 ann 的型別。
請注意,某些外來 (exotic) 或格式錯誤 (malform) 的型別物件可能沒有 __dict__ 屬性,因此為了額外的安全,你可能還希望使用 getattr() 來存取 __dict__。
手動取消字串化註釋¶
在某些註釋可能被「字串化」的情況下,並且你希望評估這些字串以產生它們表示的 Python 值,最好呼叫 inspect.get_annotations() 來為你完成這項工作。
如果你使用的是 Python 3.9 或更早版本,或者由於某種原因你無法使用 inspect.get_annotations(),則需要複製其邏輯。我們鼓勵你檢查目前 Python 版本中 inspect.get_annotations() 的實作並遵循類似的方法。
簡而言之,如果你希望評估任意物件 o 上的字串化註釋:
如果
o是一個模組,則在呼叫eval()時使用o.__dict__作為全域變數。如果
o是一個類別,當呼叫eval()時,則使用sys.modules[o.__module__].__dict__作為全域變數,使用dict(vars(o))作為區域變數。如果
o是使用functools.update_wrapper()、functools.wraps()或functools.partial()包裝的 callable ,請依據需求,透過存取o.__wrapped__或o.func來疊代解開它,直到找到根解包函式。如果
o是 callable(但不是類別),則在呼叫