アノテーションのベストプラクティス¶
オブジェクトのアノテーション辞書へのアクセス (Python 3.10 以降)¶
Python 3.10 で標準ライブラリに inspect.get_annotations() が追加されました。
Python 3.10 から 3.13 では、アノテーションに対応するオブジェクトのアノテーション辞書を取得するには、この関数を呼び出すのが推奨される方法です。
この関数では、文字列化されたアノテーションを評価して元の値に戻すこともできます。
Python 3.14 では、アノテーションを扱うためのモジュール annotationlib が追加されました。このモジュールに annotationlib.get_annotations() があり、 inspect.get_annotations() を置き換えるものです。
もし何らかの理由で自分の用途に inspect.get_annotations() が合わないという場合は、手動で __annotations__ にアクセスすることもできます。これについても、推奨のアクセス方法が Python 3.10 で変わりました。3.10 で、 o.__annotations__ は関数、クラス、モジュールに対しては必ず使えることが保証されるようになりました。オブジェクトが確実にこの3つのいずれかである場合は、単に o.__annotations__ を使えばアノテーション辞書を取得できます。
しかし、それ以外の callable (例えば、 functools.partial() で作られたもの) は __annotations__ 属性が定義されていない可能性があります。この属性があるかどうか不明なオブジェクトの場合は、Python 3.10 以降では、 getattr() に3つの引数を渡して呼び出す (例えば getattr(o, '__annotations__', None) ) のを推奨します。
Python 3.10 より前のバージョンでは、親クラスにアノテーションがあるが自身はアノテーションを定義していないようなクラスの場合、 __annotations__ を取得すると親クラスの __annotations__ が返されます。3.10 以降では、代わりに空の辞書が返されます。
オブジェクトのアノテーション辞書へのアクセス (Python 3.9 以前)¶
Python 3.9 以前では、オブジェクトのアノテーション辞書を取得するのはより複雑です。原因は、3.9 以前のバージョンの設計上の欠陥 (具体的にはクラスのアノテーション) です。
クラス以外のオブジェクト (関数や他の callable、モジュール) のアノテーションを取得する推奨の方法は、3.10と同じです。 inspect.get_annotations() ではなく、 getattr() に3つの引数を渡すことで __annotations__ 属性を取得するのがいいです。
ただし、クラスについては推奨方法が異なります。なぜなら、クラスには __annotations__ は必須ではなく、属性はベースクラスから継承されるので、 __annotations__ 属性にアクセスするとベースクラスのアノテーション辞書を取得してしまうという問題があるからです。例えば:
class Base:
a: int = 3
b: str = 'abc'
class Derived(Base):
pass
print(Derived.__annotations__)
このコードでは Derived ではなく Base のアノテーションが表示されます。
クラス (isinstance(o, type)) のアノテーションを取得するためには、新たに分岐を追加する必要があります。推奨する方法では、 Python 3.9 以前の実装に依存した方法をとります。クラスにアノテーションがあれば、 __dict__ の辞書に格納されるという実装になっています。クラスにはアノテーションがない場合もあるため、この辞書の get() メソッドを使ってアノテーションを取得する、というのが推奨の方法です。
以上を踏まえると、Python 3.9 以前では、以下のサンプルコードで任意のオブジェクトの __annotations__ 属性を安全に取得することができます:
if isinstance(o, type):
ann = o.__dict__.get('__annotations__', None)
else:
ann = getattr(o, '__annotations__', None)
このコードを実行すると、 ann の値は辞書か None となります。さらに ann の値を調べるには、 isinstance() で型を再度確認することを推奨します。
ただし、一部の特殊な型オブジェクトや不正な形式の型オブジェクトは __dict__ 属性がないこともあるため、さらに安全をとるには getattr() を使って __dict__ を取得したほうがいいです。
文字列指定のアノテーションを手動で解決する方法¶
アノテーションは文字列として指定されている場合があり、それを評価して Python の値に変換するには、 inspect.get_annotations() を使うのが一番です。
Python 3.9 以前を使っているか、もしくは何らかの理由で inspect.get_annotations() を使えない場合は、自分で実装する必要があります。現状の Python バージョンの inspect.get_annotations() の実装を確認して、同様の処理を実装することを推奨します。
すなわち、任意のオブジェクト o の文字列アノテーションを評価するには:
oがモジュールならば、o.__dict__をglobalsとしてeval()を呼ぶ。oがクラスならば、sys.modules[o.__module__].__dict__をglobalsとし、dict(vars(o))をlocalsとしてeval()を呼ぶ。oがfunctools.update_wrapper()、@functools.wraps、またはfunctools.partial()でラップされた呼び出し可能オブジェクトなら、適切なほうのo.__wrapped__またはo.funcを繰り返し参照し、ラップされていない元の関数までたどります。oがクラス以外の callable であれば、o.__globals__を globals としてeval()を呼ぶ。
しかし、必ずしもアノテーションの文字列が eval() で Python の値に変換できるとは限りません。理論上どんな文字列も指定可能であり、変換できない文字列を型ヒントとして指定する必要がある場合が実際にあります。例えば:
|を使った PEP 604 のユニオン型 (これが追加された Python 3.10 より前のバージョン)typing.TYPE_CHECKINGが True の時だけインポートされ、実行時には必要ない型定義。
eval() で上記のような値を評価しようとすると、失敗し例外が発生します。そのため、アノテーションを扱うライブラリ API を作る際は、呼び出し側が明示的に要求したときのみ、文字列を評価することが推奨されます。
__annotations__ のベストプラクティス (全 Python バージョン共通)¶
オブジェクトの
__annotations__属性に直接代入することは避けるべきです。Python に任せましょう。直接
__annotations__属性に代入するなら、必ずdictオブジェクトを代入するべきです。どういうオブジェクトかに関わらず、直接
__annotations__にアクセスするのは避けるべきです。Python 3.14 以降はannotationlib.get_annotations()、 Python 3.10 以降はinspect.get_annotations()を使いましょう。直接
__annotations__メンバーにアクセスするなら、まず値が辞書であることをチェックしてから中身を調べるべきです。__annotations__辞書を書き換えるのは避けるべきです。__annotations__属性を削除するのは避けるべきです。
__annotations__ の注意点¶
In all versions of Python 3, function
objects lazy-create an annotations dict if no annotations
are defined on that object. You can delete the __annotations__
attribute using del fn.__annotations__, but if you then
access fn.__annotations__ the object will create a new empty dict
that it will store and return as its annotations. Deleting the
annotations on a function before it has lazily created its annotations
dict will throw an AttributeError; using del fn.__annotations__
twice in a row is guaranteed to always throw an AttributeError.
Everything in the above paragraph also applies to class and module objects in Python 3.10 and newer.
Python 3 のすべてのバージョンで、関数オブジェクトの __annotations__ に None を設定できます。
ただし、その後 fn.__annotations__ でアクセスすると、この節の最初の段落で述べたとおり、空の辞書が遅延作成されます。
これは、どの Python バージョンでもモジュールやクラスには 当てはまりません。
これらのオブジェクトでは、__annotations__ に任意の Python の値を設定でき、設定した値がそのまま保持されます。
from __future__ import annotations を使って Python にアノテーションを文字列化させる場合に、アノテーションとして文字列を指定すると、その文字列自体が引用符で囲まれます。
つまり、アノテーションが 二重に 引用されます。
例えば、次のようになります。
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
これは {'a': "'str'"} を表示します。
本来は「癖」と呼ぶような動作ではありませんが、意外に感じるかもしれないため、ここで紹介しています。
独自のメタクラスを使うクラスの __annotations__ にアクセスすると、予期しない動作になる場合があります。
例は 749 を参照してください。
Python 3.14 以降では annotationlib.get_annotations()、Python 3.10 以降では inspect.get_annotations() を使うことで、こうした動作を避けられます。
それ以前の Python では、cls.__dict__.get('__annotations__', None) のように、クラスの __dict__ からアノテーションを取得することで、この問題を回避できます。
一部の Python バージョンでは、クラスのインスタンスが __annotations__ 属性を持つことがあります。
ただし、これはサポートされている機能ではありません。
インスタンスのアノテーションが必要なら、type() でそのクラスを取得できます。
例えば、Python 3.14 以降では annotationlib.get_annotations(type(myinstance)) を使えます。