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 関数で使うことを想定しています。

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.

annotationlib.type_repr(value)

任意の Python の値を、STRING 形式での使用に適した表現に変換します。 大半のオブジェクトには repr() を呼び出しますが、型オブジェクトなど一部のオブジェクトには特別な処理を行います。

これは、STRING 形式をサポートするものの、アノテーションを作成したコードにはアクセスできない、ユーザー定義の annotate 関数を補助するための関数です。 アノテーションでよく使われる値を含む他のオブジェクトについても、読みやすい文字列表現を提供するために使えます。

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.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 オブジェクトの直接作成によって、そのような入力を渡す場合が該当します。