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