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

author:
   Larry Hastings


概要
^^^^

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

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

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


オブジェクトのアノテーション辞書へのアクセス (Python 3.10 以降)
===============================================================

Python 3.10 adds a new function to the standard library:
"inspect.get_annotations()".  In Python versions 3.10 and newer,
calling this function is the best practice for accessing the
annotations dict of any object that supports annotations.  This
function can also "un-stringize" stringized annotations for you.

もし何らかの理由で自分の用途に "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" のアノテーションが表示されま
す。

Your code will have to have a separate code path if the object you're
examining is a class ("isinstance(o, type)"). In that case, best
practice relies on an implementation detail of Python 3.9 and before:
if a class has annotations defined, they are stored in the class's
"__dict__" dictionary.  Since the class may or may not have
annotations defined, best practice is to call the "get" method on the
class dict.

以上を踏まえると、Python 3.9 以前では、以下のサンプルコードで任意のオ
ブジェクトの "__annotations__" 属性を安全に取得することができます:

   if isinstance(o, type):
       ann = o.__dict__.get('__annotations__', None)
   else:
       ann = getattr(o, '__annotations__', None)

このコードを実行すると、 "ann" の値は辞書か "None" となります。さらに
"ann" の値を調べるには、 "isinstance()" で型を再度確認することを推奨し
ます。

Note that some exotic or malformed type objects may not have a
"__dict__" attribute, so for extra safety you may also wish to use
"getattr()" to access "__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" オブジェクト
  を代入するべきです。

* If you directly access the "__annotations__" member of an object,
  you should ensure that it's a dictionary before attempting to
  examine its contents.

* "__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.
