對(duì)象注解屬性的最佳實(shí)踐?

作者

Larry Hastings

摘要

本文意在匯聚對(duì)象的注解字典用法的最佳實(shí)踐。 如果 Python 代碼會(huì)去查看 Python 對(duì)象的 __annotations__ 屬性,建議遵循以下準(zhǔn)則。

本文分為四個(gè)部分:在 Python 3.10 以上版本中訪問(wèn)對(duì)象注解的最佳實(shí)踐、在Python 3.9 以上版本中訪問(wèn)對(duì)象注解的最佳實(shí)踐、適用于任何 Python 版本的其他 `__annotations__ 最佳實(shí)踐、__annotations__ 的特別之處。

請(qǐng)注意,本文是專門介紹 __annotations__ 的,而不是介紹注解的用法。若要了解“類型提示”的使用信息,請(qǐng)參閱 typing 模塊。

在 Python 3.10 以上版本中訪問(wèn)對(duì)象的注解字典?

Python 3.10 在標(biāo)準(zhǔn)庫(kù)中加入了一個(gè)新函數(shù):inspect.get_annotations()。在 Python 3.10 以上的版本中,調(diào)用該函數(shù)就是訪問(wèn)對(duì)象注解字典的最佳做法。該函數(shù)還可以“解析”字符串形式的注解。

有時(shí)會(huì)因?yàn)槟承┰蚩床坏?inspect.get_annotations() ,也可以直接訪問(wèn) __annotations__ 數(shù)據(jù)成員。這方面的最佳實(shí)踐在 Python 3.10 中也發(fā)生了變化:從 Python 3.10 開(kāi)始,Python 函數(shù)、類和模塊的 o.__annotations__ 保證 可用。如果確定是要查看這三種對(duì)象,只要利用 o.__annotations__ 讀取對(duì)象的注釋字典即可。

不過(guò)其他類型的可調(diào)用對(duì)象可能就沒(méi)有定義 __annotations__ 屬性,比如由 functools.partial() 創(chuàng)建的可調(diào)用對(duì)象。當(dāng)訪問(wèn)某個(gè)未知對(duì)象的``__annotations__`` 時(shí),Python 3.10 以上版本的最佳做法是帶三個(gè)參數(shù)去調(diào)用 getattr() ,比如 getattr(o, '__annotations__', None)。

在 Python 3.9 及更早的版本中訪問(wèn)對(duì)象的注解字典?

在 Python 3.9 及之前的版本中,訪問(wèn)對(duì)象的注解字典要比新版本中復(fù)雜得多。這個(gè)是 Python 低版本的一個(gè)設(shè)計(jì)缺陷,特別是訪問(wèn)類的注解時(shí)。

要訪問(wèn)其他對(duì)象——函數(shù)、可調(diào)用對(duì)象和模塊——的注釋字典,最佳做法與 3.10 版本相同,假定不想調(diào)用 inspect.get_annotations():你應(yīng)該用三個(gè)參數(shù)調(diào)用 getattr() ,以訪問(wèn)對(duì)象的 __annotations__ 屬性。

不幸的是,對(duì)于類而言,這并不是最佳做法。因?yàn)?`__annotations__ 是類的可選屬性,并且類可以從基類繼承屬性,訪問(wèn)某個(gè)類的 __annotations__ 屬性可能會(huì)無(wú)意間返回 基類 的注解數(shù)據(jù)。例如:

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

如此會(huì)打印出 Base 的注解字典,而非 Derived 的。

若要查看的對(duì)象是個(gè)類(isinstance(o, type)),代碼不得不另辟蹊徑。這時(shí)的最佳做法依賴于 Python 3.9 及之前版本的一處細(xì)節(jié):若某個(gè)類定義了注解,則會(huì)存放于字典 __dict__ 中。由于類不一定會(huì)定義注解,最好的做法是在類的 dict 上調(diào)用 get 方法。

綜上所述,下面給出一些示例代碼,可以在 Python 3.9 及之前版本安全地訪問(wèn)任意對(duì)象的 __annotations__ 屬性:

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

運(yùn)行之后,ann 應(yīng)為一個(gè)字典對(duì)象或 None。建議在繼續(xù)之前,先用 isinstance() 再次檢查 ann 的類型。

請(qǐng)注意,有些特殊的或畸形的類型對(duì)象可能沒(méi)有 __dict__ 屬性,為了以防萬(wàn)一,可能還需要用 getattr() 來(lái)訪問(wèn) __dict__。

解析字符串形式的注解?

有時(shí)注釋可能會(huì)被“字符串化”,解析這些字符串可以求得其所代表的 Python 值,最好是調(diào)用 inspect.get_annotations() 來(lái)完成這項(xiàng)工作。

如果是 Python 3.9 及之前的版本,或者由于某種原因無(wú)法使用 inspect.get_annotations() ,那就需要重現(xiàn)其代碼邏輯。建議查看一下當(dāng)前 Python 版本中 inspect.get_annotations() 的實(shí)現(xiàn)代碼,并遵照實(shí)現(xiàn)。

簡(jiǎn)而言之,假設(shè)要對(duì)任一對(duì)象解析其字符串化的注釋 o

  • 如果 o 是個(gè)模塊,在調(diào)用 eval() 時(shí),o.__dict__ 可視為 globals 。

  • 如果 o 是一個(gè)類,在調(diào)用 eval() 時(shí),sys.modules[o.__module__].__dict__ 視作 globals,dict(vars(o)) 視作 locals 。

  • 如果 o 是一個(gè)用 functools.update_wrapper()functools.wraps()functools.partial() 封裝的可調(diào)用對(duì)象,可酌情訪問(wèn) o.__wrapped__o.func 進(jìn)行反復(fù)解包,直到你找到未經(jīng)封裝的根函數(shù)。

  • 如果 o 是個(gè)可調(diào)用對(duì)象(但不是一個(gè)類),在調(diào)用 eval() 時(shí),o.__dict__ 可視為 globals 。

但并不是所有注解字符串都可以通過(guò) eval() 成功地轉(zhuǎn)化為 Python 值。理論上,注解字符串中可以包含任何合法字符串,確實(shí)有一些類型提示的場(chǎng)合,需要用到特殊的 無(wú)法 被解析的字符串來(lái)作注解。比如:

  • PEP 604 union types using |, before support for this was added to Python 3.10.

  • 運(yùn)行時(shí)用不到的定義,只在 typing.TYPE_CHECKING 為 True 時(shí)才會(huì)導(dǎo)入。

如果 eval() 試圖求值,將會(huì)失敗并觸發(fā)異常。因此,當(dāng)要設(shè)計(jì)一個(gè)可采用注解的庫(kù) API ,建議只在調(diào)用方顯式請(qǐng)求的時(shí)才對(duì)字符串求值。

任何版本 Python 中使用 __annotations__ 的最佳實(shí)踐?

  • 應(yīng)避免直接給對(duì)象的 __annotations__ 成員賦值。請(qǐng)讓 Python 來(lái)管理``__annotations__``。

  • 如果直接給某對(duì)象的 __annotations__ 成員賦值,應(yīng)該確保設(shè)成一個(gè)``dict`` 對(duì)象。

  • 如果直接訪問(wèn)某個(gè)對(duì)象的 __annotations__ 成員,在解析其值之前,應(yīng)先確認(rèn)其為字典類型。

  • 應(yīng)避免修改 __annotations__ 字典。

  • 應(yīng)避免刪除對(duì)象的 __annotations__ 屬性。

__annotations__ 的坑?

在 Python 3 的所有版本中,如果對(duì)象沒(méi)有定義注解,函數(shù)對(duì)象就會(huì)直接創(chuàng)建一個(gè)注解字典對(duì)象。用 del fn.__annotations__ 可刪除 __annotations__ 屬性,但如果后續(xù)再訪問(wèn) fn.__annotations__,該對(duì)象將新建一個(gè)空的字典對(duì)象,用于存放并返回注解。在函數(shù)直接創(chuàng)建注解字典前,刪除注解操作會(huì)拋出 AttributeError 異常;連續(xù)兩次調(diào)用 del fn.__annotations__ 一定會(huì)拋出一次 AttributeError 異常。

以上同樣適用于 Python 3.10 以上版本中的類和模塊對(duì)象。

所有版本的 Python 3 中,均可將函數(shù)對(duì)象的 __annotations__ 設(shè)為 None。但后續(xù)用 fn.__annotations__ 訪問(wèn)該對(duì)象的注解時(shí),會(huì)像本節(jié)第一段所述那樣,直接創(chuàng)建一個(gè)空字典。但在任何 Python 版本中,模塊和類均非如此,他們?cè)试S將 __annotations__ 設(shè)為任意 Python 值,并且會(huì)留存所設(shè)值。

如果 Python 會(huì)對(duì)注解作字符串化處理(用 from __future__ import annotations ),并且注解本身就是一個(gè)字符串,那么將會(huì)為其加上引號(hào)。實(shí)際效果就是,注解加了 兩次 引號(hào)。例如:

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

這會(huì)打印出 {'a': "'str'"}。這不應(yīng)算是個(gè)“坑”;只是因?yàn)榭赡軙?huì)讓人吃驚,所以才提一下。