對象注解屬性的最佳實踐?
- 作者
Larry Hastings
摘要
本文意在匯聚對象的注解字典用法的最佳實踐。 如果 Python 代碼會去查看 Python 對象的 __annotations__ 屬性,建議遵循以下準(zhǔn)則。
本文分為四個部分:在 Python 3.10 以上版本中訪問對象注解的最佳實踐、在Python 3.9 以上版本中訪問對象注解的最佳實踐、適用于任何 Python 版本的其他 `__annotations__ 最佳實踐、__annotations__ 的特別之處。
請注意,本文是專門介紹 __annotations__ 的,而不是介紹注解的用法。若要了解“類型提示”的使用信息,請參閱 typing 模塊。
在 Python 3.10 以上版本中訪問對象的注解字典?
Python 3.10 在標(biāo)準(zhǔn)庫中加入了一個新函數(shù):
inspect.get_annotations()。在 Python 3.10 以上的版本中,調(diào)用該函數(shù)就是訪問對象注解字典的最佳做法。該函數(shù)還可以“解析”字符串形式的注解。有時會因為某些原因看不到
inspect.get_annotations(),也可以直接訪問__annotations__數(shù)據(jù)成員。這方面的最佳實踐在 Python 3.10 中也發(fā)生了變化:從 Python 3.10 開始,Python 函數(shù)、類和模塊的o.__annotations__保證 可用。如果確定是要查看這三種對象,只要利用o.__annotations__讀取對象的注釋字典即可。不過其他類型的可調(diào)用對象可能就沒有定義
__annotations__屬性,比如由functools.partial()創(chuàng)建的可調(diào)用對象。當(dāng)訪問某個未知對象的``__annotations__`` 時,Python 3.10 以上版本的最佳做法是帶三個參數(shù)去調(diào)用getattr(),比如getattr(o, '__annotations__', None)。
在 Python 3.9 及更早的版本中訪問對象的注解字典?
在 Python 3.9 及之前的版本中,訪問對象的注解字典要比新版本中復(fù)雜得多。這個是 Python 低版本的一個設(shè)計缺陷,特別是訪問類的注解時。
要訪問其他對象——函數(shù)、可調(diào)用對象和模塊——的注釋字典,最佳做法與 3.10 版本相同,假定不想調(diào)用
inspect.get_annotations():你應(yīng)該用三個參數(shù)調(diào)用getattr(),以訪問對象的__annotations__屬性。不幸的是,對于類而言,這并不是最佳做法。因為
`__annotations__是類的可選屬性,并且類可以從基類繼承屬性,訪問某個類的__annotations__屬性可能會無意間返回 基類 的注解數(shù)據(jù)。例如:class Base: a: int = 3 b: str = 'abc' class Derived(Base): pass print(Derived.__annotations__)如此會打印出
Base的注解字典,而非Derived的。若要查看的對象是個類(
isinstance(o, type)),代碼不得不另辟蹊徑。這時的最佳做法依賴于 Python 3.9 及之前版本的一處細(xì)節(jié):若某個類定義了注解,則會存放于字典__dict__中。由于類不一定會定義注解,最好的做法是在類的 dict 上調(diào)用get方法。綜上所述,下面給出一些示例代碼,可以在 Python 3.9 及之前版本安全地訪問任意對象的
__annotations__屬性:if isinstance(o, type): ann = o.__dict__.get('__annotations__', None) else: ann = getattr(o, '__annotations__', None)運(yùn)行之后,
ann應(yīng)為一個字典對象或None。建議在繼續(xù)之前,先用isinstance()再次檢查ann的類型。請注意,有些特殊的或畸形的類型對象可能沒有
__dict__屬性,為了以防萬一,可能還需要用getattr()來訪問__dict__。
解析字符串形式的注解?
有時注釋可能會被“字符串化”,解析這些字符串可以求得其所代表的 Python 值,最好是調(diào)用
inspect.get_annotations()來完成這項工作。如果是 Python 3.9 及之前的版本,或者由于某種原因無法使用
inspect.get_annotations(),那就需要重現(xiàn)其代碼邏輯。建議查看一下當(dāng)前 Python 版本中inspect.get_annotations()的實現(xiàn)代碼,并遵照實現(xiàn)。簡而言之,假設(shè)要對任一對象解析其字符串化的注釋
o:
如果
o是個模塊,在調(diào)用eval()時,o.__dict__可視為globals。如果
o是一個類,在調(diào)用eval()時,sys.modules[o.__module__].__dict__視作globals,dict(vars(o))視作locals。如果
o是一個用functools.update_wrapper()、functools.wraps()或functools.partial()封裝的可調(diào)用對象,可酌情訪問o.__wrapped__或o.func進(jìn)行反復(fù)解包,直到你找到未經(jīng)封裝的根函數(shù)。如果
o是個可調(diào)用對象(但不是一個類),在調(diào)用eval()時,o.__dict__可視為globals。但并不是所有注解字符串都可以通過
eval()成功地轉(zhuǎn)化為 Python 值。理論上,注解字符串中可以包含任何合法字符串,確實有一些類型提示的場合,需要用到特殊的 無法 被解析的字符串來作注解。比如:
PEP 604 union types using
|, before support for this was added to Python 3.10.運(yùn)行時用不到的定義,只在
typing.TYPE_CHECKING為 True 時才會導(dǎo)入。如果
eval()試圖求值,將會失敗并觸發(fā)異常。因此,當(dāng)要設(shè)計一個可采用注解的庫 API ,建議只在調(diào)用方顯式請求的時才對字符串求值。
任何版本 Python 中使用 __annotations__ 的最佳實踐?
應(yīng)避免直接給對象的
__annotations__成員賦值。請讓 Python 來管理``__annotations__``。如果直接給某對象的
__annotations__成員賦值,應(yīng)該確保設(shè)成一個``dict`` 對象。如果直接訪問某個對象的
__annotations__成員,在解析其值之前,應(yīng)先確認(rèn)其為字典類型。應(yīng)避免修改
__annotations__字典。應(yīng)避免刪除對象的
__annotations__屬性。
__annotations__ 的坑?
在 Python 3 的所有版本中,如果對象沒有定義注解,函數(shù)對象就會直接創(chuàng)建一個注解字典對象。用
del fn.__annotations__可刪除__annotations__屬性,但如果后續(xù)再訪問fn.__annotations__,該對象將新建一個空的字典對象,用于存放并返回注解。在函數(shù)直接創(chuàng)建注解字典前,刪除注解操作會拋出AttributeError異常;連續(xù)兩次調(diào)用del fn.__annotations__一定會拋出一次AttributeError異常。以上同樣適用于 Python 3.10 以上版本中的類和模塊對象。
所有版本的 Python 3 中,均可將函數(shù)對象的
__annotations__設(shè)為None。但后續(xù)用fn.__annotations__訪問該對象的注解時,會像本節(jié)第一段所述那樣,直接創(chuàng)建一個空字典。但在任何 Python 版本中,模塊和類均非如此,他們允許將__annotations__設(shè)為任意 Python 值,并且會留存所設(shè)值。如果 Python 會對注解作字符串化處理(用
from __future__ import annotations),并且注解本身就是一個字符串,那么將會為其加上引號。實際效果就是,注解加了 兩次 引號。例如:from __future__ import annotations def foo(a: "str"): pass print(foo.__annotations__)這會打印出
{'a': "'str'"}。這不應(yīng)算是個“坑”;只是因為可能會讓人吃驚,所以才提一下。