[python] __future__.annotations をインポートすると__annotations__が文字列になる
ちょっとはまったので備忘録.
環境: windows python 3.11.4, 3.10.5
背景
from dataclasses import dataclass
@dataclass
class A:
f1: str
@dataclass
class B:
f2: A
f3: int | None
if __name__ == "__main__":
print(B.__annotations__)
上記コードを実行すると以下のような出力になる
{'f2': <class '__main__.A'>, 'f3': int | None}
ちゃんとクラスAの情報やUnionTypeの型情報が取れてる。正しい。
では元のコードにfrom __future__ import annotations
を付け加えて実行してみる。すると以下のような出力になる。
{'f2': 'A', 'f3': 'int | None'}
クラスではなく文字列になってる。なんで???
なんで????
注釈 get_type_hints() does not work with imported type aliases that include forward references. Enabling postponed evaluation of annotations (PEP 563) may remove the need for most forward references.
とあり、さらにPEP563をみると
This PEP proposes changing function annotations and variable annotations so that they are no longer evaluated at function definition time. Instead, they are preserved in
__annotations__
in string form.
このPEPは、関数アノテーションと変数アノテーションを変更して、関数定義時に評価されないようにすることを提案しています。代わりに、それらは文字列形式で__annotations__
に保存されます。(google翻訳)
This change is being introduced gradually, starting with a
__future__
import in Python 3.7.
とあるので、from __future__ import annotations
をimportすると、PEPで定められているように関数/変数アノテーションは__annotations__
に文字列で格納される。
To resolve an annotation at runtime from its string form to the result of the enclosed expression, user code needs to evaluate the string.
For code that uses type hints, the typing.get_type_hints(obj, globalns=None, localns=None) function correctly evaluates expressions back from its string form. Note that all valid code currently using annotations should already be doing that since a type annotation can be expressed as a string literal.
For code which uses annotations for other purposes, a regular eval(ann, globals, locals) call is enough to resolve the annotation.
__annotations__
に格納された文字列の型アノテーション情報をクラスとして使いたいなら文字列を評価する必要があり、
-
typing.get_type_hints()
を使う -
eval(ann, globals, locals)
を呼び出す
などすればよい
get_type_hints()
を使う
回避策: obj.__annotations__
を直接取るのではなく、typing.get_type_hints()
を使う。
from __future__ import annotations
from dataclasses import dataclass
from typing import get_type_hints
@dataclass
class A:
f1: str
@dataclass
class B:
f2: A
f3: int | None
if __name__ == "__main__":
print(B.__annotations__)
print(get_type_hints(B))
{'f2': 'A', 'f3': 'int | None'}
{'f2': <class '__main__.A'>, 'f3': int | None}
注意: 継承してる場合
get_type_hints()
は対象のobjectのmroを遡ってすべての__annotations__
を統合して出力する
from __future__ import annotations
from dataclasses import dataclass
from typing import get_type_hints
@dataclass
class A:
f1: str
@dataclass
class B(A):
f2: A
f3: int | None
if __name__ == "__main__":
print(B.__annotations__)
print(get_type_hints(B))
{'f2': 'A', 'f3': 'int | None'}
{'f1': <class 'str'>, 'f2': <class '__main__.A'>, 'f3': int | None}
なので対象のオブジェクトの__annotations__
だけ出力しようとすると追加の処理が必要
def get_obj_type_hints(obj):
return {
key: value
for key, value in get_type_hints(obj).items()
if key in getattr(obj, "__annotations__", {})
}
if __name__ == "__main__":
print(B.__annotations__)
print(get_type_hints(B))
print(get_obj_type_hints(B))
{'f2': 'A', 'f3': 'int | None'}
{'f1': <class 'str'>, 'f2': <class '__main__.A'>, 'f3': int | None}
{'f2': <class '__main__.A'>, 'f3': int | None}
注意その2: アノテーションにNoneを指定した場合
型アノテーションにNone
を付けた場合(そんなことするケースがあるか知らないが)、
__annotations__
とget_type_hints()
で出力される値が違う
from dataclasses import dataclass
from typing import get_type_hints
@dataclass
class A:
f1: None
f2: int
def get_obj_type_hints(obj):
return {
key: value
for key, value in get_type_hints(obj).items()
if key in getattr(obj, "__annotations__", {})
}
if __name__ == "__main__":
print(A.__annotations__)
print(get_type_hints(A))
# from __future__ import annotations を付けた場合
{'f1': 'None'}
{'f1': <class 'NoneType'>}
# from __future__ import annotations を付けない場合
{'f1': None}
{'f1': <class 'NoneType'>}
型アノテーションなのでNoneTypeになってるほうが正しい気がする・・・が正確に__annotations__
の情報が欲しいのであればeval()
で評価してやる必要がある
from __future__ import annotations
from dataclasses import dataclass
from typing import get_type_hints
@dataclass
class A:
f1: None
f2: int
def get_hints(obj):
annotations = getattr(obj, "__annotations__", {})
for key, value in annotations.items():
if isinstance(value, str):
annotations[key] = eval(value)
else:
continue
return annotations
if __name__ == "__main__":
print(A.__annotations__)
print(get_type_hints(A))
print(get_hints(A))
# __future__をimportした時の__annotations__
{'f1': 'None', 'f2': 'int'}
# get_type_hints()した結果
{'f1': <class 'NoneType'>, 'f2': <class 'int'>}
# __annotations__の値をeval()した結果
{'f1': None, 'f2': <class 'int'>}
ちなみに__future__のインポートなしの場合以下のように出力される
{'f1': None, 'f2': <class 'int'>}
{'f1': <class 'NoneType'>, 'f2': <class 'int'>}
{'f1': None, 'f2': <class 'int'>}
ただしクラスが読み込まれてない場合などevalするとそんなクラスないよとエラーが返る場合があるので注意が必要。
get_type_hints()
するとその辺ちゃんと解決してくらたりするので、eval呼ぶより素直にget_type_hints()
するほうが良いと思います。
終わりに
typingモジュールにはほかにgenericalias.__origin__
を取ってくるtyping.get_origin()
とかgenericalias.__args__
を取ってくるtyping.get_args()
とかがあって、必要か?これ。という感想だったがこういうことがあると直接dunder変数取るのではなく、こういう組み込みの関数呼んだほうがいい気がするので気を付けましょう。でもはまりどころもあったので注意してね。
Discussion