Pythonの型ヒントについて勉強しているときに、理解に時間がかかったことをまとめようと思います。
型ヒントとは
Pythonの型ヒントとは、定義した変数や関数の引数や返り値の型を注釈することです。 例えば、valという変数にはintしか入れたくないという場合には、
val : int val = 1
という書き方をします。このとき、注意しなければいけないことは、「valはint型である」という情報はあくまで注釈にすぎないということです。 したがって、次のような書き方をしてもエラーの出力はありません。
val : int val = "1"
上記の例では、valにint型であると注釈をつけたのに、valに文字列型を入れてしまっています。しかし、Pythonは動的言語であり、かつ「valはintである」という情報はあくまで注釈にすぎないため、エラーを出力することはありません。
Mypy
型ヒントの型と異なる型を変数に代入してもエラーは出力されないと上で書きました。それでは、型ヒントは何に役に立つのでしょうか? キーワードはMypyです。 宣言した型と異なる型の値が変数に代入されたときに、Mypyはエラーを出力してくれます。
例として次のGistを見てください。なおコマンド中のマジックコマンドはPythonの型定義の方法とは?型ヒントについてもわかりやすく解説に記載があったものを利用しています。
上記のGistの中で、In[2]を見ると型ヒントの型と異なる型を変数に代入しています。しかし、Pythonを実行する上ではエラーを出力することもなく、普通に実行できています。
次に、In[3]を見てください。In[3]ではMypyによるチェックが行われています。 Mypyのチェックを行うと、型ヒントの型と変数に代入した型が異なる場合に、エラーを出力しています。
以上のように、型ヒントはPythonを実行する上では影響を与えませんが、Mypyによって静的型付けのチェックをすることが可能になります。
理解に時間がかかったこと
typing.cast
typing.castメソッドを使い、型付けを行う方法があります。これについて、私は最初typing.castは型を変更するメソッドだと勘違いしていました。なので、下記のようなスクリプトを書いて、「なぜval2はstr型にならずint型のままなのだろう~」と考えていました。
しかし、実際はtyping.castメソッドは型チェッカー(今回はMypy)に型を伝えるだけの存在です。例えば次のスクリプトはMypyによるエラー出力があります。
%%typecheck --ignore-missing-imports from typing import cast from __future__ import annotations x:int|str = 1 def kronecker_delta(y:int) -> int: if y >= 0 | y <= 1: return y return 0 print(kronecker_delta(x))
これはxがint|str型(intとstrの合併型)であるのに対し、kronecker_deltaメソッドの引数がint型であるためにMypyはエラー出力をします。このような場合に、typing.castは役に立ちます。次のスクリプトだとMypyによるエラー出力はありません。
%%typecheck --ignore-missing-imports from typing import cast from __future__ import annotations x:int|str = 1 def kronecker_delta(y:int) -> int: if y >= 0 | y <= 1: return y return 0 x = cast(int, x) print(kronecker_delta(x))
この場合は、xを最初にint|str型で指定しましたが、castによってint型であるとMypyに伝えています。そのため、kronecker_deltaの引数の型との矛盾が発生せず、Mypyはエラー出力をしません。
このことをイメージしたものが図1です。typing.castは型を変換するのではなく、型チェッカ―に型を教えるメソッドと私は理解しました。
typing.overload
メソッドの引数と返り値に型ヒントを与える方法として下記の書き方があります。
def union(x:int|str, y:int|str) -> int|str: return x+y var2: int = union(x=1, y=1) var11: str = union(x="1", y="1")
しかし、この書き方だとMypyは次のエラーを出力します。
error: Incompatible types in assignment (expression has type "Union[int, str]", variable has type "int")
error: Incompatible types in assignment (expression has type "Union[int, str]", variable has type "str")
つまり、var2の出力がint型、var11はstr型なのに、unionの返り値はint|str型だとMypyは主張します。 このようなときに役に立つのがtyping.overloadです。
上記のメソッドunionはx, yがint型のときは出力は必ずint型になります。x, yがstr型のときは必ずstr型です。これを意識して書いたスクリプトが下記です。
from typing import overload @overload def union(x:int, y:int) -> int: ... @overload def union(x:str, y:str) -> str: ... def union(x, y): return x+y
Mypyのための型ヒントをつけたメソッドを定義し(@overloadをつけたメソッド)、その後ろに処理を定義したメソッドを書くというのが@overloadの使い方です。
まとめ
- typing.castは型を変換するメソッドではなく、型チェッカーに型を伝えるためのメソッド
- typing.overloadは引数と返り値の型が1対1対応するメソッドを定義し、引数と返り値に合併型を使わないメソッドを構成するためのデコレータ