初心者データサイエンティストの備忘録

調べたことは全部ここに書いて自分の辞書を作る

【Python】型ヒントの理解で苦しんだこと

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は型を変換するのではなく、型チェッカ―に型を教えるメソッドと私は理解しました。

図1:Mypyが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対応するメソッドを定義し、引数と返り値に合併型を使わないメソッドを構成するためのデコレータ

参考文献