註釋 (annotation) 最佳實踐¶
- 作者:
Larry Hastings
摘要
本文件旨在封裝 (encapsulate) 使用註釋字典 (annotations dicts) 的最佳實踐。如果你寫 Python 程式碼並在調查 Python 物件上的__annotations__
,我們鼓勵你遵循下面描述的準則。
本文件分為四個部分:在 Python 3.10 及更高版本中存取物件註釋的最佳實踐、在 Python 3.9 及更早版本中存取物件註釋的最佳實踐、適用於任何Python 版本__annotations__
的最佳實踐,以及__annotations__
的奇異之處。
請注意,本文件是特別說明__annotations__
的使用,而非如何使用註釋。如果你正在尋找如何在你的程式碼中使用「型別提示 (type hint)」的資訊,請查閱模組 (module)typing
。
在 Python 3.10 及更高版本中存取物件的註釋字典¶
Python 3.10 在標準函式庫中新增了一個新函式:inspect.get_annotations()
。在 Python 3.10 及更高版本中,呼叫此函式是存取任何支援註釋的物件的註釋字典的最佳實踐。此函式也可以為你「取消字串化 (un-stringize)」字串化註釋。
若由於某種原因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__
屬性可能會無意中回傳基底類別的註釋字典。舉例來說:
classBase:a:int=3b:str='abc'classDerived(Base):passprint(Derived.__annotations__)
這將印出 (print) 來自Base
的註釋字典,而不是Derived
。
如果你正在檢查的物件是一個類別 (isinstance(o,type)
),你的程式碼將必須有一個單獨的程式碼路徑。在這種情況下,最佳實踐依賴 Python 3.9 及之前版本的實作細節 (implementation detail):如果一個類別定義了註釋,它們將儲存在該類別的__dict__
字典中。由於類別可能定義了註釋,也可能沒有定義,因此最佳實踐是在類別字典上呼叫get()
方法。
總而言之,以下是一些範例程式碼,可以安全地存取 Python 3.9 及先前版本中任意物件上的__annotations__
屬性:
ifisinstance(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(但不是類別),則在呼叫eval()
時使用o.__globals__
作為全域變數。
然而,並非所有用作註釋的字串值都可以透過eval()
成功轉換為 Python 值。理論上,字串值可以包含任何有效的字串,並且在實踐中,型別提示存在有效的用例,需要使用特定「無法」評估的字串值進行註釋。例如:
在 Python 3.10 支援PEP 604 聯合型別 (union type)
|
之前使用它。Runtime 中不需要的定義,僅在
typing.TYPE_CHECKING
為 true 時匯入。
如果eval()
嘗試計算這類型的值,它將失敗並引發例外。因此,在設計使用註釋的函式庫 API 時,建議僅在呼叫者 (caller) 明確請求時嘗試評估字串值。
任何 Python 版本中__annotations__
的最佳實踐¶
你應該避免直接指派給物件的
__annotations__
成員。讓 Python 管理設定__annotations__
。如果你直接指派給物件的
__annotations__
成員,則應始終將其設為dict
物件。如果直接存取物件的
__annotations__
成員,則應在嘗試檢查其內容之前確保它是字典。你應該避免修改
__annotations__
字典。你應該避免刪除物件的
__annotations__
屬性。
__annotations__
奇異之處¶
在 Python 3 的所有版本中,如果沒有在該物件上定義註釋,則函式物件會延遲建立 (lazy-create) 註釋字典。你可以使用delfn.__annotations__
刪除__annotations__
屬性,但如果你隨後存取fn.__annotations__
,該物件將建立一個新的空字典,它將作為註釋儲存並傳回。在函式延遲建立註釋字典之前刪除函式上的註釋將拋出AttributeError
;連續兩次使用delfn.__annotations__
保證總是拋出AttributeError
。
上一段的所有內容也適用於 Python 3.10 及更高版本中的類別和模組物件。
在 Python 3 的所有版本中,你可以將函式物件上的__annotations__
設定為None
。但是,隨後使用fn.__annotations__
存取該物件上的註釋將根據本節第一段的內容延遲建立一個空字典。對於任何 Python 版本中的模組和類別來說,情況並非如此;這些物件允許將__annotations__
設定為任何 Python 值,並且將保留設定的任何值。
如果 Python 為你字串化你的註釋(使用from__future__importannotations
),並且你指定一個字串作為註釋,則該字串本身將被引用。實際上,註釋被引用了兩次。例如:
from__future__importannotationsdeffoo(a:"str"):passprint(foo.__annotations__)
這會印出{'a':"'str'"}
。這不應該被認為是一個「奇異的事」,他在這裡被簡單提及,因為他可能會讓人意想不到。