annotationlib
— 用於內省註解的功能¶
在 3.14 版本加入。
原始碼: Lib/annotationlib.py
annotationlib
模組提供了用於內省模組、類和函式上的 註解 的工具。
註解是 惰性求值 的,並且通常包含對建立註解時尚未定義的物件的正向引用。該模組提供了一組底層工具,可用於以可靠的方式檢索註解,即使存在正向引用和其他邊緣情況。
此模組支援以三種主要格式檢索註解(參見 Format
),每種格式最適合不同的用例
VALUE
對註解求值並返回其值。這是最直接的使用方式,但可能會引發錯誤,例如當註解包含對未定義名稱的引用時。FORWARDREF
為無法解析的註解返回ForwardRef
物件,允許您在不求值的情況下檢查註解。當您需要處理可能包含未解析正向引用的註解時,這很有用。STRING
以字串形式返回註解,類似於它在原始檔中出現的方式。這對於希望以可讀方式顯示註解的文件生成器很有用。
get_annotations()
函式是用於檢索註解的主要入口點。給定一個函式、類或模組,它會返回所請求格式的註解字典。此模組還提供了直接使用用於求值註解的 註解函式 的功能,例如 get_annotate_from_class_namespace()
和 call_annotate_function()
,以及用於處理 求值函式 的 call_evaluate_function()
函式。
注意
此模組中的大多數功能可以執行任意程式碼;有關更多資訊,請參閱安全部分。
參見
PEP 649 提出了當前 Python 中註解工作方式的模型。
PEP 749 擴充套件了 PEP 649 的各個方面,並引入了 annotationlib
模組。
註解最佳實踐 提供了使用註解的最佳實踐。
typing-extensions 提供了 get_annotations()
的向後移植版本,可在早期版本的 Python 上執行。
註解的語義¶
註解的求值方式在 Python 3 的歷史中發生了變化,目前仍然依賴於 future 匯入。註解曾有過以下執行模型:
標準語義(Python 3.0 到 3.13 的預設行為;參見 PEP 3107 和 PEP 526):註解在原始碼中遇到時會立即求值。
字串化註解(在 Python 3.7 及更高版本中與
from __future__ import annotations
一起使用;參見 PEP 563):註解僅以字串形式儲存。延遲求值(Python 3.14 及更高版本的預設行為;參見 PEP 649 和 PEP 749):註解是惰性求值的,僅在訪問時才求值。
例如,考慮以下程式
def func(a: Cls) -> None:
print(a)
class Cls: pass
print(func.__annotations__)
其行為如下
在標準語義下(Python 3.13及更早版本),它會在定義
func
的那一行丟擲NameError
,因為此時Cls
是一個未定義的名稱。在字串化註解下(如果使用了
from __future__ import annotations
),它會列印{'a': 'Cls', 'return': 'None'}
。在延遲求值下(Python 3.14及更高版本),它會列印
{'a': <class 'Cls'>, 'return': None}
。
當函式註解在 Python 3.0 中首次引入時(由 PEP 3107),使用了標準語義,因為這是實現註解最簡單、最直接的方式。當變數註解在 Python 3.6 中引入時(由 PEP 526),也使用了相同的執行模型。然而,當使用註解作為型別提示時,標準語義會引起問題,例如需要引用在註解遇到時尚未定義的名稱。此外,在模組匯入時執行註解存在效能問題。因此,在 Python 3.7 中,PEP 563 引入了使用 from __future__ import annotations
語法將註解儲存為字串的功能。當時的計劃是最終將此行為設為預設,但出現了一個問題:對於在執行時內省註解的人來說,字串化註解更難處理。一個替代提案,PEP 649,引入了第三種執行模型,即延遲求值,並在 Python 3.14 中實現。如果存在 from __future__ import annotations
,則仍使用字串化註解,但此行為最終將被移除。
類¶
- class annotationlib.Format¶
一個
IntEnum
,描述了可以返回註解的格式。該列舉的成員或其等效的整數值可以傳遞給get_annotations()
和此模組中的其他函式,以及__annotate__
函式。- VALUE = 1¶
值是求值註解表示式的結果。
- VALUE_WITH_FAKE_GLOBALS = 2¶
用於表示註解函式正在具有虛假全域性變數的特殊環境中求值的特殊值。當傳遞此值時,註解函式應返回與
Format.VALUE
格式相同的值,或引發NotImplementedError
以表示它們不支援在此環境中執行。此格式僅在內部使用,不應傳遞給此模組中的函式。
- FORWARDREF = 3¶
對於已定義的值,值為真實的註解值(根據
Format.VALUE
格式);對於未定義的值,值為ForwardRef
代理。真實物件可能包含對ForwardRef
代理物件的引用。
- STRING = 4¶
值是註解在原始碼中出現的文字字串,可能會有包括但不限於空白規範化和常量值最佳化在內的修改。
這些字串的確切值在 Python 的未來版本中可能會改變。
在 3.14 版本加入。
- class annotationlib.ForwardRef¶
一個用於註解中正向引用的代理物件。
當使用
FORWARDREF
格式且註解包含無法解析的名稱時,將返回此類的例項。當在註解中使用正向引用時,例如在定義類之前引用該類,就可能發生這種情況。- __forward_arg__¶
一個包含被求值以產生
ForwardRef
的程式碼的字串。該字串可能與原始原始碼不完全等價。
- evaluate(*, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE)¶
求值正向引用,返回其值。
如果 format 引數是
VALUE
(預設值),此方法可能會丟擲異常,例如NameError
,如果正向引用指向一個無法解析的名稱。此方法的引數可用於為本來未定義的名稱提供繫結。如果 format 引數是FORWARDREF
,該方法將永遠不會丟擲異常,但可能返回一個ForwardRef
例項。例如,如果正向引用物件包含程式碼list[undefined]
,其中undefined
是一個未定義的名稱,使用FORWARDREF
格式求值它將返回list[ForwardRef('undefined')]
。如果 format 引數是STRING
,該方法將返回__forward_arg__
。owner 引數為此方法傳遞作用域資訊提供了首選機制。
ForwardRef
的所有者是包含該ForwardRef
派生自的註解的物件,例如模組物件、型別物件或函式物件。globals、locals 和 type_params 引數提供了一種更精確的機制,用於影響求值
ForwardRef
時可用的名稱。globals 和 locals 會傳遞給eval()
,表示求值名稱時所在的全域性和區域性名稱空間。type_params 引數與使用 泛型類 和 函式 的原生語法建立的物件相關。它是在求值正向引用時作用域內的 型別形參 元組。例如,如果要求值從泛型類C
的類名稱空間中找到的註解中檢索到的ForwardRef
,type_params 應設定為C.__type_params__
。由
get_annotations()
返回的ForwardRef
例項保留了對其來源作用域資訊的引用,因此不帶任何其他引數呼叫此方法可能足以求值此類物件。透過其他方式建立的ForwardRef
例項可能沒有任何關於其作用域的資訊,因此可能需要向此方法傳遞引數才能成功求值它們。如果沒有提供 owner、globals、locals 或 type_params,並且
ForwardRef
不包含關於其來源的資訊,則使用空的全域性和區域性字典。
在 3.14 版本加入。
函式¶
- annotationlib.annotations_to_string(annotations)¶
將包含執行時值的註解字典轉換為僅包含字串的字典。如果值不是字串,則使用
type_repr()
進行轉換。這旨在作為使用者提供的註解函式的輔助工具,這些函式支援STRING
格式,但無法訪問建立註解的程式碼。例如,這用於為透過函式式語法建立的
typing.TypedDict
類實現STRING
:>>> from typing import TypedDict >>> Movie = TypedDict("movie", {"name": str, "year": int}) >>> get_annotations(Movie, format=Format.STRING) {'name': 'str', 'year': 'int'}
在 3.14 版本加入。
- annotationlib.call_annotate_function(annotate, format, *, owner=None)¶
使用給定的 format(
Format
列舉的成員)呼叫註解函式 annotate,並返回該函式生成的註解字典。需要此輔助函式是因為編譯器為函式、類和模組生成的註解函式在直接呼叫時僅支援
VALUE
格式。為了支援其他格式,此函式在一個特殊的環境中呼叫註解函式,使其能夠以其他格式生成註解。在實現需要在類構建過程中部分求值註解的功能時,這是一個有用的構建塊。owner 是擁有註解函式的物件,通常是函式、類或模組。如果提供,它將在
FORWARDREF
格式中用於生成攜帶更多資訊的ForwardRef
物件。參見
PEP 649 中包含了對此函式所用實現技術的解釋。
在 3.14 版本加入。
- annotationlib.call_evaluate_function(evaluate, format, *, owner=None)¶
使用給定的 format(
Format
列舉的成員)呼叫求值函式 evaluate,並返回該函式生成的值。這與call_annotate_function()
類似,但後者總是返回一個將字串對映到註解的字典,而此函式返回單個值。這旨在與為類型別名和型別形參相關的惰性求值元素生成的求值函式一起使用
typing.TypeVar.evaluate_bound()
,型別變數的邊界typing.TypeVar.evaluate_default()
,型別變數的預設值typing.ParamSpec.evaluate_default()
,引數規格的預設值typing.TypeVarTuple.evaluate_default()
,型別變數元組的預設值
owner 是擁有求值函式的物件,例如類型別名或型別變數物件。
format可用於控制返回值的格式
>>> type Alias = undefined >>> call_evaluate_function(Alias.evaluate_value, Format.VALUE) Traceback (most recent call last): ... NameError: name 'undefined' is not defined >>> call_evaluate_function(Alias.evaluate_value, Format.FORWARDREF) ForwardRef('undefined') >>> call_evaluate_function(Alias.evaluate_value, Format.STRING) 'undefined'
在 3.14 版本加入。
- annotationlib.get_annotate_from_class_namespace(namespace)¶
從類名稱空間字典 namespace 中檢索註解函式。如果名稱空間不包含註解函式,則返回
None
。這主要在類完全建立之前(例如,在元類中)有用;類存在後,可以使用cls.__annotate__
檢索註解函式。有關在元類中使用此函式的示例,請參見下文。在 3.14 版本加入。
- annotationlib.get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)¶
計算一個物件的註解字典。
obj 可以是可呼叫物件、類、模組或其他具有
__annotate__
或__annotations__
屬性的物件。傳遞任何其他物件會引發TypeError
。format 引數控制返回註解的格式,並且必須是
Format
列舉的成員或其等效整數。不同格式的工作方式如下VALUE: 首先嚐試
object.__annotations__
;如果不存在,則在存在的情況下呼叫object.__annotate__
函式。FORWARDREF: 如果
object.__annotations__
存在且可以成功求值,則使用它;否則,呼叫object.__annotate__
函式。如果它也不存在,則再次嘗試object.__annotations__
並重新引發訪問它時產生的任何錯誤。STRING: 如果
object.__annotate__
存在,則首先呼叫它;否則,使用object.__annotations__
並使用annotations_to_string()
將其字串化。
返回一個字典。每次呼叫
get_annotations()
都會返回一個新字典;在同一個物件上呼叫它兩次將返回兩個不同但等價的字典。此函式為您處理了幾個細節
如果 eval_str 為 true,則型別為
str
的值將使用eval()
進行反字串化。這旨在與字串化註解(from __future__ import annotations
)一起使用。將 eval_str 設定為 true 並使用除Format.VALUE
之外的格式是錯誤的。如果 obj 沒有註解字典,則返回一個空字典。(函式和方法總是有一個註解字典;類、模組和其他型別的可呼叫物件可能沒有。)
忽略類上繼承的註解,以及元類上的註解。如果一個類沒有自己的註解字典,則返回一個空字典。
為了安全起見,所有對物件成員和字典值的訪問都是使用
getattr()
和dict.get()
完成的。
eval_str 控制是否將型別為
str
的值替換為對這些值呼叫eval()
的結果如果 eval_str 為 true,則對型別為
str
的值呼叫eval()
。(注意get_annotations()
不會捕獲異常;如果eval()
引發異常,它將展開堆疊,越過get_annotations()
呼叫。)如果 eval_str 為 false(預設值),則型別為
str
的值保持不變。
globals 和 locals 會傳遞給
eval()
;有關更多資訊,請參閱eval()
的文件。如果 globals 或 locals 為None
,此函式可能會根據type(obj)
將該值替換為特定於上下文的預設值如果 obj 是一個模組,globals 預設為
obj.__dict__
。如果 obj 是一個類,globals 預設為
sys.modules[obj.__module__].__dict__
,locals 預設為 obj 類的名稱空間。如果 obj 是一個可呼叫物件,globals 預設為
obj.__globals__
,但如果 obj 是一個包裝函式(使用functools.update_wrapper()
)或一個functools.partial
物件,它會被解包直到找到一個未包裝的函式。
呼叫
get_annotations()
是訪問任何物件註解字典的最佳實踐。有關注解最佳實踐的更多資訊,請參閱註解最佳實踐。>>> def f(a: int, b: str) -> float: ... pass >>> get_annotations(f) {'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}
在 3.14 版本加入。
範例¶
在元類中使用註解¶
元類 可能希望在類建立期間檢查甚至修改類主體中的註解。這樣做需要從類名稱空間字典中檢索註解。對於使用 from __future__ import annotations
建立的類,註解將位於字典的 __annotations__
鍵中。對於其他帶註解的類,可以使用 get_annotate_from_class_namespace()
獲取註解函式,並使用 call_annotate_function()
呼叫它並檢索註解。使用 FORWARDREF
格式通常是最好的,因為這允許註解引用在建立類時尚無法解析的名稱。
要修改註解,最好建立一個包裝器註解函式,該函式呼叫原始註解函式,進行任何必要的調整,然後返回結果。
下面是一個元類的示例,它從類中過濾掉所有 typing.ClassVar
註解,並將它們放在一個單獨的屬性中
import annotationlib
import typing
class ClassVarSeparator(type):
def __new__(mcls, name, bases, ns):
if "__annotations__" in ns: # from __future__ import annotations
annotations = ns["__annotations__"]
classvar_keys = {
key for key, value in annotations.items()
# Use string comparison for simplicity; a more robust solution
# could use annotationlib.ForwardRef.evaluate
if value.startswith("ClassVar")
}
classvars = {key: annotations[key] for key in classvar_keys}
ns["__annotations__"] = {
key: value for key, value in annotations.items()
if key not in classvar_keys
}
wrapped_annotate = None
elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
annotations = annotationlib.call_annotate_function(
annotate, format=annotationlib.Format.FORWARDREF
)
classvar_keys = {
key for key, value in annotations.items()
if typing.get_origin(value) is typing.ClassVar
}
classvars = {key: annotations[key] for key in classvar_keys}
def wrapped_annotate(format):
annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
return {key: value for key, value in annos.items() if key not in classvar_keys}
else: # no annotations
classvars = {}
wrapped_annotate = None
typ = super().__new__(mcls, name, bases, ns)
if wrapped_annotate is not None:
# Wrap the original __annotate__ with a wrapper that removes ClassVars
typ.__annotate__ = wrapped_annotate
typ.classvars = classvars # Store the ClassVars in a separate attribute
return typ
STRING
格式的侷限性¶
STRING
格式旨在近似註解的原始碼,但所使用的實現策略意味著並不總是可能恢復確切的原始碼。
首先,字串化器當然無法恢復編譯後代碼中不存在的任何資訊,包括註釋、空白、括號以及被編譯器簡化的操作。
其次,字串化器可以攔截幾乎所有涉及在某個作用域中查詢名稱的操作,但它無法攔截完全對常量進行操作的操作。因此,這也意味著在不受信任的程式碼上請求 STRING
格式是不安全的:Python 足夠強大,即使無法訪問任何全域性變數或內建函式,也可能實現任意程式碼執行。例如
>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
...
>>> annotationlib.get_annotations(f, format=annotationlib.Format.STRING)
Hello world
{'x': 'None'}
備註
這個特定示例在撰寫本文時有效,但它依賴於實現細節,不保證將來仍然有效。
在 Python 中存在的不同種類的表示式中,如 ast
模組所表示的,一些表示式是受支援的,這意味著 STRING
格式通常可以恢復原始原始碼;其他則不受支援,意味著它們可能導致不正確的輸出或錯誤。
以下是受支援的(有時有注意事項)
-
ast.Invert
(~
),ast.UAdd
(+
), 和ast.USub
(-
) 是受支援的ast.Not
(not
) 不受支援
ast.Dict
(使用**
解包時除外)ast.Call
(使用**
解包時除外)ast.Constant
(雖然不是常量的確切表示;例如,字串中的轉義序列會丟失;十六進位制數會轉換為十進位制)ast.Attribute
(假設值不是常量)ast.Subscript
(假設值不是常量)ast.Starred
(*
解包)
以下是不受支援的,但在字串化器遇到時會丟擲一個資訊性錯誤
ast.FormattedValue
(f-字串;如果使用!r
等轉換說明符,則不會檢測到錯誤)ast.JoinedStr
(f-字串)
以下是不受支援的,並導致不正確的輸出
以下在註解作用域中是不允許的,因此不相關
FORWARDREF
格式的侷限性¶
FORWARDREF
格式旨在儘可能地生成真實值,任何無法解析的內容都用 ForwardRef
物件替換。它受到與 STRING
格式大致相同的限制:對字面量執行操作或使用不受支援的表示式型別的註解,在使用 FORWARDREF
格式求值時可能會引發異常。
以下是一些關於不受支援表示式行為的例子
>>> from annotationlib import get_annotations, Format
>>> def zerodiv(x: 1 / 0): ...
>>> get_annotations(zerodiv, format=Format.STRING)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> get_annotations(zerodiv, format=Format.FORWARDREF)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> def ifexp(x: 1 if y else 0): ...
>>> get_annotations(ifexp, format=Format.STRING)
{'x': '1'}
內省註解的安全影響¶
此模組中的許多功能都涉及執行與註解相關的程式碼,這些程式碼可以執行任意操作。例如,get_annotations()
可能會呼叫任意的 註解函式,而 ForwardRef.evaluate()
可能會對任意字串呼叫 eval()
。註解中包含的程式碼可能會進行任意的系統呼叫、進入無限迴圈或執行任何其他操作。這也適用於任何對 __annotations__
屬性的訪問,以及 typing
模組中處理註解的各種函式,例如 typing.get_type_hints()
。
由此產生的任何安全問題也立即適用於匯入可能包含不受信任註解的程式碼之後:匯入程式碼總是可能導致執行任意操作。然而,從不受信任的來源接受字串或其他輸入,並將其傳遞給任何用於內省註解的 API 是不安全的,例如透過編輯 __annotations__
字典或直接建立 ForwardRef
物件。