註解最佳實踐¶
- 作者:
Larry Hastings
在 Python 3.10 及更高版本中訪問物件的註解字典¶
Python 3.10 在標準庫中添加了一個新函式:inspect.get_annotations()
。在 Python 3.10 到 3.13 版本中,呼叫此函式是訪問任何支援註解的物件的註解字典的最佳實踐。此函式還可以為您“反字串化”字串化的註解。
在 Python 3.14 中,有一個新的 annotationlib
模組,其中包含用於處理註解的功能。這包括一個 annotationlib.get_annotations()
函式,它取代了 inspect.get_annotations()
。
如果由於某種原因 inspect.get_annotations()
不適用於您的用例,您可以手動訪問 __annotations__
資料成員。從 Python 3.10 開始,最佳實踐也發生了變化:o.__annotations__
保證 始終 適用於 Python 函式、類和模組。如果您確定您正在檢查的物件是這三個 特定 物件之一,您可以直接使用 o.__annotations__
來獲取物件的註解字典。
然而,其他型別的可呼叫物件——例如,由 functools.partial()
建立的可呼叫物件——可能沒有定義 __annotations__
屬性。在訪問可能未知物件的 __annotations__
時,Python 3.10 及更高版本中的最佳實踐是呼叫帶有三個引數的 getattr()
,例如 getattr(o, '__annotations__', None)
。
在 Python 3.10 之前,訪問未定義註解但其父類有註解的類的 __annotations__
將返回父類的 __annotations__
。在 Python 3.10 及更高版本中,子類的註解將是一個空字典。
在 Python 3.9 及更早版本中訪問物件的註解字典¶
在 Python 3.9 及更早版本中,訪問物件的註解字典比在新版本中複雜得多。問題在於這些舊版本的 Python 中的一個設計缺陷,特別是與類註解有關的缺陷。
訪問其他物件(函式、其他可呼叫物件和模組)的註解字典的最佳實踐與 3.10 版本的最佳實踐相同,前提是您沒有呼叫 inspect.get_annotations()
:您應該使用帶有三個引數的 getattr()
來訪問物件的 __annotations__
屬性。
不幸的是,這對於類來說不是最佳實踐。問題在於,由於 __annotations__
在類上是可選的,並且由於類可以從其基類繼承屬性,訪問類的 __annotations__
屬性可能會無意中返回 基類 的註解字典。例如
class Base:
a: int = 3
b: str = 'abc'
class Derived(Base):
pass
print(Derived.__annotations__)
這將列印 Base
的註解字典,而不是 Derived
的。
如果您正在檢查的物件是一個類 (isinstance(o, type)
),您的程式碼將不得不有一個單獨的程式碼路徑。在這種情況下,最佳實踐依賴於 Python 3.9 及以前版本的實現細節:如果一個類定義了註解,它們將儲存在類的 __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
的型別。
請注意,某些奇異或格式錯誤的型別物件可能沒有 __dict__
屬性,因此為了額外安全,您可能還需要使用 getattr()
來訪問 __dict__
。
手動反字串化字串化註解¶
在某些註解可能被“字串化”的情況下,並且您希望評估這些字串以生成它們所代表的 Python 值,最好還是呼叫 inspect.get_annotations()
來為您完成這項工作。
如果您正在使用 Python 3.9 或更早版本,或者由於某種原因您無法使用 inspect.get_annotations()
,您將需要複製其邏輯。建議您檢查當前 Python 版本中 inspect.get_annotations()
的實現並遵循類似的方法。
簡而言之,如果您希望評估任意物件 o
上的字串化註解
如果
o
是一個模組,在呼叫eval()
時使用o.__dict__
作為globals
。如果
o
是一個類,在呼叫eval()
時使用sys.modules[o.__module__].__dict__
作為globals
,並使用dict(vars(o))
作為locals
。如果
o
是一個使用functools.update_wrapper()
、functools.wraps()
或functools.partial()
包裝的可呼叫物件,透過訪問o.__wrapped__
或o.func
迭代地解包它,直到找到根未包裝函式。如果
o
是一個可呼叫物件(但不是類),在呼叫eval()
時使用o.__globals__
作為全域性變數。
然而,並非所有用作註解的字串值都可以透過 eval()
成功轉換為 Python 值。理論上,字串值可以包含任何有效字串,實際上,有些型別提示的有效用例需要使用 無法 評估的字串值進行註解。例如
PEP 604 聯合型別使用
|
,在 Python 3.10 中新增支援之前。在執行時不需要的定義,僅當
typing.TYPE_CHECKING
為真時才匯入。
如果 eval()
嘗試評估這些值,它將失敗並引發異常。因此,在設計處理註解的庫 API 時,建議僅在呼叫者明確請求時才嘗試評估字串值。
適用於任何 Python 版本的 __annotations__
最佳實踐¶
您應該避免直接為物件的
__annotations__
成員賦值。讓 Python 管理__annotations__
的設定。如果您確實直接為物件的
__annotations__
成員賦值,您應該始終將其設定為dict
物件。您應該避免直接訪問任何物件上的
__annotations__
。相反,請使用annotationlib.get_annotations()
(Python 3.14+) 或inspect.get_annotations()
(Python 3.10+)。如果您確實直接訪問物件的
__annotations__
成員,您應該在嘗試檢查其內容之前確保它是一個字典。您應該避免修改
__annotations__
字典。您應該避免刪除物件的
__annotations__
屬性。
__annotations__
怪癖¶
在所有 Python 3 版本中,如果物件上沒有定義註解,函式物件會惰性建立註解字典。您可以使用 del fn.__annotations__
刪除 __annotations__
屬性,但如果您隨後訪問 fn.__annotations__
,物件將建立一個新的空字典,並將其儲存並作為其註解返回。在函式惰性建立其註解字典之前刪除函式上的註解將引發 AttributeError
;連續兩次使用 del fn.__annotations__
保證始終引發 AttributeError
。
上一段中的所有內容也適用於 Python 3.10 及更高版本中的類和模組物件。
在所有 Python 3 版本中,您可以將函式物件上的 __annotations__
設定為 None
。然而,隨後使用 fn.__annotations__
訪問該物件上的註解將根據本節第一段的規定惰性建立空字典。這對於任何 Python 版本中的模組和類都 不 適用;這些物件允許將 __annotations__
設定為任何 Python 值,並將保留設定的任何值。
如果 Python 為您字串化註解(使用 from __future__ import annotations
),並且您將字串指定為註解,則該字串本身將被引用。實際上,註解被引用了 兩次。例如
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
這將列印 {'a': "'str'"}
。這不應該真正被視為“怪癖”;這裡提到它只是因為它可能令人驚訝。
如果您使用具有自定義元類的類並訪問類上的 __annotations__
,您可能會觀察到意外行為;有關一些示例,請參見 749。您可以透過在 Python 3.14+ 上使用 annotationlib.get_annotations()
或在 Python 3.10+ 上使用 inspect.get_annotations()
來避免這些怪癖。在早期版本的 Python 中,您可以透過從類的 __dict__
訪問註解來避免這些錯誤(例如,cls.__dict__.get('__annotations__', None)
)。
在某些 Python 版本中,類的例項可能具有 __annotations__
屬性。然而,這不是支援的功能。如果您需要例項的註解,您可以使用 type()
訪問其類(例如,在 Python 3.14+ 上使用 annotationlib.get_annotations(type(myinstance))
)。