アノテーションのベストプラクティス¶
- author:
Larry Hastings
オブジェクトのアノテーション辞書へのアクセス (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()でラップされた callable ならば、一番内部の関数に到達するまで、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.
In all versions of Python 3, you can set __annotations__
on a function object to None. However, subsequently
accessing the annotations on that object using fn.__annotations__
will lazy-create an empty dictionary as per the first paragraph of
this section. This is not true of modules and classes, in any Python
version; those objects permit setting __annotations__ to any
Python value, and will retain whatever value is set.
If Python stringizes your annotations for you
(using from __future__ import annotations), and you
specify a string as an annotation, the string will
itself be quoted. In effect the annotation is quoted
twice. For example:
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
This prints {'a': "'str'"}. This shouldn't really be considered
a "quirk"; it's mentioned here simply because it might be surprising.
If you use a class with a custom metaclass and access __annotations__
on the class, you may observe unexpected behavior; see
749 for some examples. You can avoid these
quirks by using annotationlib.get_annotations() on Python 3.14+ or
inspect.get_annotations() on Python 3.10+. On earlier versions of
Python, you can avoid these bugs by accessing the annotations from the
class's __dict__
(for example, cls.__dict__.get('__annotations__', None)).
In some versions of Python, instances of classes may have an __annotations__
attribute. However, this is not supported functionality. If you need the
annotations of an instance, you can use type() to access its class
(for example, annotationlib.get_annotations(type(myinstance)) on Python 3.14+).