🔍

Jupyter で型ヒントをやってみた

2022/02/05に公開

mypy を使った Jupyter での 型ヒントついて調べたのでメモとして残します。

コードはこちら

ファイル構成

|-- 実行ファイル.ipynb
|-- mypy.ini # mypy が対応していないサードパーティ製のライブラリを無視する設定を追加します。

mypy.ini ファイル

[mypy-ライブラリ名]
ignore_missing_imports = True

とすることで、サードパーティ製のライブラリがmypyのチェックに引っ掛からなくなります。

[mypy]
[mypy-numpy]
ignore_missing_imports = True
[mypy-pandas]
ignore_missing_imports = True
[mypy-nptyping]
ignore_missing_imports = True
[mypy-IPython.core.display]
ignore_missing_imports = True

mypy による 型ヒントの実行

import numpy as np
import pandas as pd
from IPython.display import display

from typing import List, Tuple, Dict
from nptyping import NDArray, Object

import warnings
warnings.simplefilter('ignore')

セル内で下記コマンドを実行すれば、型ヒントチェックが可能です。

!nbqa mypy test.ipynb
test.ipynb:cell_3:1: error: Incompatible types in assignment (expression has type "str", variable has type "int")
test.ipynb:cell_4:1: error: List item 0 has incompatible type "str"; expected "int"
test.ipynb:cell_4:1: error: List item 1 has incompatible type "str"; expected "int"
test.ipynb:cell_4:1: error: List item 2 has incompatible type "str"; expected "int"
test.ipynb:cell_5:1: error: Incompatible types in assignment (expression has type "Tuple[str, str, str]", variable has type "Tuple[int]")
test.ipynb:cell_6:1: error: Dict entry 0 has incompatible type "int": "str"; expected "str": "int"
test.ipynb:cell_6:1: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "int"
test.ipynb:cell_6:1: error: Dict entry 2 has incompatible type "int": "str"; expected "str": "int"
test.ipynb:cell_7:3: error: Incompatible return value type (got "int", expected "str")
test.ipynb:cell_9:1: error: Argument 1 to "change_str_to_num1" has incompatible type "int"; expected "str"
Found 10 errors in 1 file (checked 1 source file)

ただし、mypy では numpy や Pandas の型を直接チェックすることはできないようです。

これについては後ほど触れます。

型ヒントの設定

変数の型ヒントは コロン(:)の後に型を設定すれば良いです。

int型 の型ヒントを設定しているのに文字列を代入しているので mypy 実行時にエラーとして検出されます。

num :int = "abc"

リスト、タプル、辞書型の型ヒント

リストや辞書の場合は typing ライブラリを使う必要があります。

下記はリストに int型の型ヒントを設定しているのに文字列を代入しているので mypy 実行時にエラーとして検出されます。

from typing import List

num_list :List[int] = ["a", "b", "c"]

タプルの場合は下記の通り。
これも mypy でエラーとして検出されます。

from typing import Tuple

num_tuple :Tuple[int] = ("a", "b", "c")

辞書は下記の通り、これも mypy でエラーとして検出されます。

from typing import Dict

str_num_dict :Dict[str, int] = {1:"a", 2:"b", 3:"c"}

関数の場合は、引数については今までと同様ですが、返り値については -> の後に型を設定します。

この場合、返り値に str を設定しているのに、返り値は int型になるので mypy でエラーとして検出されます。

def change_str_to_num1(num_str: str) -> str:
    num = int(num_str)
    return num

引数に str を設定しているのに、int型を代入した場合も mypy でエラーとして検出されます。

change_str_to_num1(1)

numpy の型ヒント

先述の通り、numpy や pandas の場合は mypy で型ヒントのエラーを検出することができません。

nptyping ライブラリの NDArray と isintance を使うことで、型と形状を確認することができます。

想定通りであれば True、間違っていれば False を返します。

これと assert を組み合わせて、想定した型と形状出なかった場合はエラーとなるようにできます。

from nptyping import NDArray

num_array = np.array(["a", "b", "c"])
print(num_array)

assert isinstance(num_array, NDArray[3, int])

下記のように、アサーションエラーとなります。

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [11], in <module>
      1 num_array = np.array(["a", "b", "c"])
      2 print(num_array)
----> 4 assert isinstance(num_array, NDArray[3, int])

AssertionError: 

いちいち形状を指定するのが面倒な場合は、len() や shape を使うといいでしょう。

下記もアサーションエラーとなります。

str_array = np.array(["str", "str", "str"])
print(str_array)

# 型チェック
assert isinstance(str_array, NDArray[len(str_array), int])

下記はあっているのでエラーは検出されません。

double_nim_array = np.array([[1, 2], [2, 3]])
print(double_nim_array)

# 型チェック
assert isinstance(double_nim_array, NDArray[double_nim_array.shape, int])

なお、pytest を使ってアサーションエラーを一括検出できないかなと試してみましたが、あまりにも重かったので断念しました...。

pandas の型ヒント

下記のような、int型の数値で構成された DataFrame型の変数を作成します。

df = pd.DataFrame(
    [
        [1, 2, 3],
        [4, 5, 6]
    ],
    columns=["a", "b", "c"],
    dtype=int,
)
display(df)

.values メソッドで DataFrame型を np.array型に変えてやることで、numpy と同じように型チェックすることが可能です。

下記の場合はアサーションエラーは発生しません。

assert isinstance(df.values, NDArray[df.values.shape, int])

下記の場合はアサーションエラーとなります。

assert isinstance(df.values, NDArray[df.values.shape, str])
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [16], in <module>
----> 1 assert isinstance(df.values, NDArray[df.values.shape, str])

AssertionError: 

DataFrame型はカラムごとに別の型を持つことができるので、下記のように for文を使えばカラムごとの型チェックができます。

下記の場合はアサーションエラーとなりません。

for column_name in df.columns:
    print(column_name, df[column_name].values)
    assert isinstance(df[column_name].values, NDArray[df[column_name].values.shape, int])
a [1 4]
b [2 5]
c [3 6]

下記の場合はアサーションエラーとなります。

エラー内容にカラム名が出力されるようにしておくと便利そうです。

for column_name in df.columns:
    print(column_name, df[column_name].values)
    assert isinstance(df[column_name].values, NDArray[df[column_name].values.shape, str]), "カラム名:" + column_name
a [1 4]
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [18], in <module>
      1 for column_name in df.columns:
      2     print(column_name, df[column_name].values)
----> 3     assert isinstance(df[column_name].values, NDArray[df[column_name].values.shape, str]), "カラム名:" + column_name

AssertionError: カラム名:a

カラムごとの型をバラバラにするため、c カラムを str (Object)型に変換してみます。

df["c"] = df["c"].astype(str)

さきほどと同様の処理ですが、下記のようにアサーションエラーとなります。

for column_name in df.columns:
    print(column_name, df[column_name].values)
    assert isinstance(df[column_name].values, NDArray[df[column_name].values.shape, int]), column_name
a [1 4]
b [2 5]
c ['3' '6']
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [20], in <module>
      1 for column_name in df.columns:
      2     print(column_name, df[column_name].values)
----> 3     assert isinstance(df[column_name].values, NDArray[df[column_name].values.shape, int]), column_name

AssertionError: c

そこで、チェックしたい型のリストを作成します。

なお、型を変換するときは str を使いましたが、チェックするときは str ではなく、nptyping ライブラリの Object とする必要がありますので注意しましょう。

from nptyping import Object

type_list = [int, int, Object]

for i, column_name in enumerate(df.columns):
    print(column_name, df[column_name].values, df[column_name].dtypes, type_list[i])
    assert isinstance(df[column_name].values, NDArray[df[column_name].values.shape, type_list[i]]), column_name
a [1 4] int64 <class 'int'>
b [2 5] int64 <class 'int'>
c ['3' '6'] object Object

以上になります、最後までお読みいただきありがとうございました。

参考サイト

https://yumarublog.com/python/typehint/

https://koh-sh.hatenablog.com/entry/2020/07/04/224716

https://qiita.com/qython/items/1f7416bbb29f48a153bb

https://qiita.com/syoyo/items/40b7e0b76bbcf8722184

Discussion