アノテーションのベストプラクティス
**********************************


概要
^^^^

このドキュメントは、アノテーションの辞書を扱う上でのベストプラクティス
をまとめるものです。Python オブジェクトに付与された "__annotations__"
を Python コードからアクセスする際には、以下のガイドラインに従うことを
推奨します。

本ドキュメントは次の４セクションで構成されています。Python 3.10 以降で
オブジェクトのアノテーションにアクセスするときのベストプラクティス、
Python 3.9 以下でオブジェクトのアノテーションにアクセスするときのベス
トプラクティス、Python バージョンに関わらない "__annotations__" に関す
るベストプラクティス、 "__annotations__" の注意点、です。

注意：本ドキュメントは、アノテーションの使い方ではなく、
"__annotations__" の使い方を扱っています。型ヒントをコードの中でどうや
って使うかについては、 "typing" モジュールを参照してください。


オブジェクトのアノテーション辞書へのアクセス (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__" は関数、クラス、モジュールに対しては必ず使え
ることが保証されるようになりました。オブジェクトが確実にこの３つのいず
れかである場合は、単に "o.__annotations__" を使えばアノテーション辞書
を取得できます。

しかし、それ以外の callable (例えば、 "functools.partial()" で作られた
もの) は "__annotations__" 属性が定義されていない可能性があります。こ
の属性があるかどうか不明なオブジェクトの場合は、Python 3.10 以降では、
"getattr()" に３つの引数を渡して呼び出す (例えば "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()" に３つの引数を渡す
ことで "__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))" を使えます。
