argparse-dataclass使用時に__future__.annotationsをimportするとエラーが出るので中身を調べてみた

argparse-dataclassはdataclassをargparseに合う形に変換して渡してくれるもの。
https://github.com/mivade/argparse_dataclass

もともとargparseを使っていたのだが、以下の理由から引数を管理するクラスを作りたくなった。

  • package化して外から使える形にする際に便利
  • 引数の設定だけまとめてあると管理しやすい

本当はhydraを使ってみようと思ったのだが、そのときやりたいことからすると過剰な感じだった(dataclassとして定義さえできればOKだったので)

ということで真っ先に目についたのが名前の通りそのままのargparse-dataclassだったのだが、少し使ってみたところタイトルの通り__future__.annotationsをimportすることでエラーが出る。

例えばこちらの設定に対して

@dataclass
class Config:
    cpu: bool = False

以下のエラー

ValueError: 'bool' is not callable

エラーを出力してる箇所はこちら
https://github.com/python/cpython/blob/v3.7.12/Lib/argparse.py#L1364
で、type というキーで登録しているのは元を辿るとこれ
https://github.com/mivade/argparse_dataclass/blob/8051690/argparse_dataclass.py#L185

ということで

  • metadataとしてtypeが定義されていればそれ type として使う
  • なければdataclassの型アノテーションとして書いたものを使う
  • argparse側ではそれがcallableであることが想定されている
  • が、実際はただの文字列として渡っている

今回はmetadataの定義はしてなかったので、型アノテーションとして書いたものが使われている。なので解決策としてはmetadataとして以下のように型を渡せばOK。

@dataclass
class Config:
    cpu: bool = field(default=False, metadata=dict(type=bool)) 

ただ、型アノテーションとして書かれたものだとなぜうまくいかないのか気になるので追ってみる。__future__.annotationsをimportしなければ問題なく通ることから、元々は組み込み型のboolコンストラクタとして使える状態だったはず。

ということで__future__.annotationsが実際に何をしているのかを見る
https://www.python.org/dev/peps/pep-0563/

PEP 563 -- Postponed Evaluation of Annotations

Just like default values, annotations are evaluated at function definition time. This creates a number of issues for the type hinting use case:

  • forward references: when a type hint contains names that have not been defined yet, that definition needs to be expressed as a string literal

  • type hints are executed at module import time, which is not computationally free.

Postponing the evaluation of annotations solves both problems.

現状だとアノテーションの評価タイミングが関数定義の時点だがそれを遅らせるというのが趣旨。それによって定義前の段階の型へのアクセスやimport時に型評価が実行されることによる計算リソースの無駄遣いをなくせる、というメリットがある。

ではいつ評価されるようになるのかというと、

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.

type hintを実行時に型として扱うにはユーザ側で評価の処理を実行する必要があり、それにはtyping.get_type_hintsを使えばよい。

というわけで、 タイトルに書いたようにエラーが出てしまうのは、argparse-dataclass側でそもそも__annotations__に対する対応が入ってないためで、適用するにはtype hintの型を評価するコードを入れる必要があるということだった。

そもそもannotationsはこちらの記事を参考にして型アノテーションをpython3.9以降と同様の形でかけるようにするという目的で入れていた。
https://future-architect.github.io/articles/20201223/

中身についてはちゃんと調べていなかったので今回はとても勉強になった。annotationsをimportすることでlist[str]などのように型名をそのままアノテーションとして使えるようになるというのも、型として評価されなくなるために可能になることだと考えられる。