annotationlib --- アノテーションを調べるための機能¶
Added in version 3.14.
ソースコード: Lib/annotationlib.py
annotationlib モジュールは、モジュール、クラス、関数の アノテーション を調べるためのツールを提供します。
アノテーションは 遅延評価 され、作成時にはまだ定義されていないオブジェクトへの前方参照を含むことがよくあります。 このモジュールは、前方参照やその他の特殊な場合があっても、アノテーションを確実に取得するための低水準のツール群を提供します。
このモジュールは、アノテーションを取得するための三つの主要な形式をサポートします (Format を参照)。
それぞれ適した用途が異なります。
VALUEは、アノテーションを評価してその値を返します。 最も直接的に扱える形式ですが、未定義の名前を参照するアノテーションなどでは、エラーが発生する場合があります。FORWARDREFは、解決できないアノテーションに対してForwardRefオブジェクトを返すので、それらを評価せずに調べられます。 未解決の前方参照を含む可能性のあるアノテーションを扱う場合に便利です。STRINGは、ソースファイルでの表記に近い文字列としてアノテーションを返します。 アノテーションを読みやすく表示するドキュメント生成ツールなどに便利です。
アノテーションを取得する主な入口は get_annotations() 関数です。
関数、クラス、モジュールを渡すと、指定した形式のアノテーション辞書を返します。
このモジュールには、アノテーションの評価に使う annotate 関数 を直接扱う get_annotate_from_class_namespace() や call_annotate_function()、および evaluate 関数 を扱う call_evaluate_function() もあります。
注意
このモジュールの大半の機能では、任意のコードが実行される可能性があります。 詳しくは セキュリティの節 を参照してください。
参考
現在の Python のアノテーションの動作モデルは PEP 649 で提案されました。
PEP 749 は PEP 649 のさまざまな側面を詳しく規定し、annotationlib モジュールを導入しました。
アノテーションを扱う際の推奨方法は アノテーションのベストプラクティス を参照してください。
typing-extensions は、以前の Python バージョンでも使える get_annotations() のバックポートを提供します。
アノテーションの意味論¶
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¶
annotate 関数が、仮のグローバル名前空間を持つ特別な環境で評価されていることを示す特別な値です。 この値を渡された annotate 関数は、
Format.VALUE形式と同じ値を返すか、この環境での実行をサポートしないことを示すNotImplementedErrorを送出する必要があります。 この形式は内部でのみ使われ、このモジュールの関数に渡すべきではありません。
- FORWARDREF = 3¶
定義済みの値には実際のアノテーションの値 (
Format.VALUE形式と同じ)、未定義の値にはForwardRefプロキシを使います。 実際のオブジェクトがForwardRefプロキシオブジェクトへの参照を含むこともあります。
- STRING = 4¶
値は、ソースコード内のアノテーションの表記を文字列にしたものです。 ただし、空白の正規化や定数値の最適化などの変更が加わる場合があり、変更はこれらに限りません。
これらの文字列の厳密な値は、将来の Python バージョンで変わる可能性があります。
Added in version 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に元のスコープの情報がない場合、グローバルとローカルの名前空間には空の辞書を使います。
Added in version 3.14.
関数¶
- annotationlib.annotations_to_string(annotations)¶
実行時の値を含むアノテーション辞書を、文字列だけを含む辞書に変換します。 値がまだ文字列でなければ、
type_repr()で変換します。 これは、STRING形式をサポートするものの、アノテーションを作成したコードにはアクセスできない、ユーザー定義の annotate 関数を補助するための関数です。例えば、関数形式の構文で作成した
typing.TypedDictクラスのSTRING形式は、この関数で実装されています。>>> from typing import TypedDict >>> Movie = TypedDict("movie", {"name": str, "year": int}) >>> get_annotations(Movie, format=Format.STRING) {'name': 'str', 'year': 'int'}
Added in version 3.14.
- annotationlib.call_annotate_function(annotate, format, *, owner=None)¶
annotate 関数
annotateを、Format列挙型のメンバーであるformatを指定して呼び出し、その関数が生成したアノテーション辞書を返します。関数、クラス、モジュールのためにコンパイラが生成する annotate 関数は、直接呼び出した場合には
VALUE形式しかサポートしないため、この補助関数が必要です。 他の形式をサポートするため、この関数は特別な環境で annotate 関数を呼び出し、それらの形式のアノテーションを生成できるようにします。 クラスの作成途中でアノテーションを部分的に評価する機能を実装する際に役立ちます。ownerはアノテーション関数を所有するオブジェクトで、通常は関数、クラス、モジュールのいずれかです。 指定すると、FORWARDREF形式で、より多くの情報を持つForwardRefオブジェクトを生成するために使われます。参考
この関数で使われている実装手法は、PEP 649 で説明されています。
Added in version 3.14.
- annotationlib.call_evaluate_function(evaluate, format, *, owner=None)¶
evaluate 関数
evaluateを、Format列挙型のメンバーであるformatを指定して呼び出し、その関数が生成した値を返します。call_annotate_function()と似ていますが、そちらは常に文字列をアノテーションに対応付ける辞書を返すのに対し、この関数は一つの値を返します。この関数は、型エイリアスや型パラメーターに関係する、遅延評価される要素のために生成された evaluate 関数で使うことを想定しています。
typing.TypeAliasType.evaluate_value(): 型エイリアスの値typing.TypeVar.evaluate_bound(): 型変数の上限typing.TypeVar.evaluate_default(): 型変数のデフォルト値typing.ParamSpec.evaluate_default(): パラメーター仕様のデフォルト値typing.TypeVarTuple.evaluate_default(): 型変数タプルのデフォルト値
ownerは evaluate 関数を所有するオブジェクトです。 型エイリアスや型変数のオブジェクトなどが該当します。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'
Added in version 3.14.
- annotationlib.get_annotate_from_class_namespace(namespace)¶
クラスの名前空間辞書
namespaceから annotate 関数 を取得します。 名前空間に annotate 関数がなければNoneを返します。 主に、クラスの作成が完了する前 (メタクラス内など) に役立ちます。 クラスができた後は、cls.__annotate__で annotate 関数を取得できます。 メタクラスでこの関数を使う例は 後述 します。Added in version 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__を試し、アクセス時のエラーを再送出します。object.__annotate__は、まずFORWARDREFを指定して呼び出します。 その形式が実装されていなければ、VALUE_WITH_FAKE_GLOBALSのサポートを確認し、仮のグローバル名前空間を持つ環境で使います。 どちらの形式もサポートされていなければ、VALUEを使います。VALUEでの呼び出しが失敗した場合は、そのエラーを送出します。
STRING:
object.__annotate__があれば、まずそれを呼び出します。 なければobject.__annotations__を使い、annotations_to_string()で文字列化します。object.__annotate__は、まずSTRINGを指定して呼び出します。 その形式が実装されていなければ、VALUE_WITH_FAKE_GLOBALSのサポートを確認し、仮のグローバル名前空間を持つ環境で使います。 どちらの形式もサポートされていなければ、VALUEを使い、その結果をannotations_to_string()で変換します。VALUEでの呼び出しが失敗した場合は、そのエラーを送出します。
辞書を返します。
get_annotations()は呼び出すたびに新しい辞書を返します。 同じオブジェクトに対して 2 回呼び出すと、内容は同じでも別々の辞書が返されます。この関数は、次の細部も処理します。
eval_strが真の場合、str型の値をeval()で評価し、文字列化される前の値に戻します。 これは、文字列化されたアノテーション (from __future__ import annotations) で使うことを想定しています。Format.VALUE以外の形式でeval_strを真にすると、エラーになります。objにアノテーション辞書がなければ、空の辞書を返します。 (関数とメソッドには常にアノテーション辞書がありますが、クラス、モジュール、他の種類の呼び出し可能オブジェクトにはない場合があります。)クラスが継承したアノテーションと、メタクラスのアノテーションは無視します。 クラス自体にアノテーション辞書がなければ、空の辞書を返します。
安全のため、オブジェクトのメンバーと辞書の値へのアクセスには、すべて
getattr()とdict.get()を使います。
eval_strは、str型の値を、その値に対してeval()を呼び出した結果に置き換えるかどうかを制御します。eval_strが真の場合、str型の値に対してeval()を呼び出します。 (get_annotations()は例外を捕捉しません。eval()が例外を送出すると、get_annotations()の呼び出し元まで伝播します。)eval_strが偽 (デフォルト) の場合、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'>}
Added in version 3.14.
レシピ¶
メタクラスでのアノテーションの利用¶
メタクラス では、クラスの作成中にクラス本体のアノテーションを調べたり変更したりしたい場合があります。
そのためには、クラスの名前空間辞書からアノテーションを取得する必要があります。
from __future__ import annotations を使って作成されたクラスでは、アノテーションは辞書の __annotations__ キーにあります。
それ以外のアノテーションを持つクラスでは、get_annotate_from_class_namespace() で annotate 関数を取得し、call_annotate_function() で呼び出してアノテーションを取得できます。
クラスの作成時にはまだ解決できない名前をアノテーションで参照できるよう、通常は FORWARDREF 形式を使うのが最適です。
アノテーションを変更するには、元の annotate 関数を呼び出して必要な調整を加え、結果を返すラッパーの annotate 関数を作るのがよい方法です。
次の例は、クラスからすべての 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()
# 簡単にするため文字列を比較する。より堅牢な方法には
# 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: # アノテーションなし
classvars = {}
wrapped_annotate = None
typ = super().__new__(mcls, name, bases, ns)
if wrapped_annotate is not None:
# 元の __annotate__ を、ClassVar を取り除くラッパーで包む
typ.__annotate__ = wrapped_annotate
typ.classvars = classvars # ClassVar を別の属性に格納する
return typ
独自の呼び出し可能オブジェクトによる annotate 関数の作成¶
独自の annotate 関数 は、関数、クラス、モジュール用に自動生成されるものと同じように、通常の関数で実装できます。 クラスによるカプセル化を使いたい場合には、任意の 呼び出し可能オブジェクト を annotate 関数 として使えます。
VALUE、STRING、FORWARDREF 形式を直接提供するには、annotate 関数 に次の属性が必要です。
__call__(format, /) -> dictというシグネチャの呼び出し可能な__call__。 サポートする形式を指定して呼び出した場合に、NotImplementedErrorを送出しないもの。
VALUE_WITH_FAKE_GLOBALS 形式は、STRING や FORWARDREF が直接サポートされていない場合に、それらを自動生成するために使われます。
この形式を提供する annotate 関数 には、次の属性が必要です。
__call__(format, /) -> dictというシグネチャの呼び出し可能な__call__。VALUE_WITH_FAKE_GLOBALSを指定して呼び出した場合に、NotImplementedErrorを送出しないもの。annotate 関数のコンパイル済みコードを含む コードオブジェクト
__code__。任意:
__code__が表す関数が位置引数のデフォルト値を使う場合、その値のタプル__defaults__。任意:
__code__が表す関数がキーワード引数のデフォルト値を使う場合、その値の辞書__kwdefaults__。任意: その他すべての 関数の属性。
class Annotate:
called_formats = []
def __call__(self, format=None, /, *, _self=None):
# 仮のグローバル名前空間で呼ぶ場合、_self が実際の self の値、
# self が形式になる。
if _self is not None:
self, format = _self, self
self.called_formats.append(format)
if format <= 2: # VALUE または VALUE_WITH_FAKE_GLOBALS
return {"x": MyType}
raise NotImplementedError
__code__ = __call__.__code__
__defaults__ = (None,)
__kwdefaults__ = property(lambda self: dict(_self=self))
__globals__ = {}
__builtins__ = {}
__closure__ = None
次のように呼び出せます。
>>> from annotationlib import call_annotate_function, Format
>>> call_annotate_function(Annotate(), format=Format.STRING)
{'x': 'MyType'}
また、オブジェクトの annotate 関数としても使えます。
>>> from annotationlib import get_annotations, Format
>>> class C:
... pass
>>> C.__annotate__ = Annotate()
>>> get_annotations(Annotate(), format=Format.STRING)
{'x': 'MyType'}
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'}
注釈
この例は執筆時点では動作しますが、実装の詳細に依存しており、将来も動作する保証はありません。
ast モジュールで表される Python のさまざまな式のうち、一部はサポートされており、STRING 形式で元のソースコードをおおむね復元できます。
その他の式はサポートされておらず、誤った出力やエラーを生じる場合があります。
次のものがサポートされています (一部には注意事項があります)。
-
ast.Invert(~)、ast.UAdd(+)、ast.USub(-) はサポートされています。ast.Not(not) はサポートされていません。
ast.Dict(**によるアンパックを使う場合を除く)ast.Call(**によるアンパックを使う場合を除く)ast.Constant(ただし、定数の厳密な表記は保持されません。例えば、文字列内のエスケープシーケンスは失われ、16 進数は 10 進数に変換されます)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() は任意の annotate 関数 を呼び出す可能性があり、ForwardRef.evaluate() は任意の文字列に対して eval() を呼び出す可能性があります。
アノテーション内のコードは、任意のシステムコールを実行したり、無限ループに入ったり、その他のあらゆる操作を行ったりするかもしれません。
この点は、__annotations__ 属性へのあらゆるアクセスや、typing.get_type_hints() など、アノテーションを扱う typing モジュールの関数にも当てはまります。
このことから生じるセキュリティの問題は、信頼できないアノテーションを含む可能性があるコードをインポートした直後にも当てはまります。
コードのインポートでは、常に任意の操作が実行され得るためです。
ただし、信頼できない入力元から文字列などを受け取り、アノテーションを調べる API に渡すのは安全ではありません。
例えば、__annotations__ 辞書の編集や、ForwardRef オブジェクトの直接作成によって、そのような入力を渡す場合が該当します。