概要
Python3.5以降のPEP484で追加された型ヒント。
numpyのndarrayにも適用できないかと思い、型ヒント静的チェックツールであるmypyや、サードパーティモジュールへの対処などwp調べた結果についてまとめる。
結論
結論からいうと、mypyを用いたnumpy.ndarrayの型ヒントチェックはnumpy-stubsを用いれば可能。
ただし、現(2020年1月)時点ではndarrayのdtype, shapeを指定してのmypyチェックはできない。
一方、dtype, shapeを含めた型ヒントを可読性のためのアノテーションとしてつけたいというのであれば、nptypingを用いるという選択肢がよさそう。
準備
最低限以下をpip
でインストールしておく。(カッコ内は筆者の検証時の環境)
- numpy (1.18.1)
- mypy (0.761)
mypyによる型チェック
まず型チェックとは何ぞやという状態だったので実験してみた。
# ex1.py
from typing import List, Tuple
def calc_center(points: List[Tuple[int, int]]) -> Tuple[float, float]:
'''点のリストから重心を求める'''
n = len(points)
x, y = 0, 0
for p in points:
x += p[0]
y += p[1]
return x/n, y/n
points_invalid = [[1, 1], [4, 2], [3, 6], [-1, 3]]
print(calc_center(points_invalid)) # TypeHint Error
上記のコード、もちろん正常終了するが、mypyで型ヒントチェックしてみる。
pip等でmypyをインストールしていれば、ターミナルでmypy
を実行できるはずだ。
>mypy ex1.py
ex1.py:16: error: Argument 1 to "calc_center" has incompatible type "List[List[int]]"; expected "List[Tuple[int, int]]"
Found 1 error in 1 file (checked 1 source file)
>python ex1.py
(1.75, 3.0)
このように、型ヒントが違反している箇所を指摘してくれる。以下のように修正するとmypyによるチェックは通る。
# ex2.py
from typing import List, Tuple
def calc_center(points: List[Tuple[int, int]]) -> Tuple[float, float]:
'''点のリストから重心を求める'''
n = len(points)
x, y = 0, 0
for p in points:
x += p[0]
y += p[1]
return x/n, y/n
points = [(1, 1), (4, 2), (3, 6), (-1, 3)]
print(calc_center(points)) # Success
>mypy ex2.py
Success: no issues found in 1 source file
サードパーティモジュールに対する型チェック
座標点の計算などはnumpyのndarrayが便利であるのでそのように変更したい。
しかし、先ほどのコードにimport numpy
を追加してmypyを走らすと以下エラーが出てしまう。
# ex3.py
from typing import List, Tuple
import numpy as np
def calc_center(points: List[Tuple[int, int]]) -> Tuple[float, float]:
'''点のリストから重心を求める'''
n = len(points)
x, y = 0, 0
for p in points:
x += p[0]
y += p[1]
return x/n, y/n
points = [(1, 1), (4, 2), (3, 6), (-1, 3)]
print(calc_center(points)) # Success
>mypy ex3.py
ex3.py:4: error: No library stub file for module 'numpy'
ex3.py:4: note: (Stub files are from https://github.com/python/typeshed)
Found 1 error in 1 file (checked 1 source file)
エラーの原因はnumpyパッケージ自体が型ヒントに対応してないからである。
じゃあどうすんのということで以下3つのケースで対策を分けてみる。
方法1. サードパーティ製の型ヒントは無視
これが一番楽な方法で一般的なようだ。
やり方は、mypy.iniという名前でファイルを作成し、以下のように記述した後カレントディレクトリに置く。
[mypy]
[mypy-numpy]
ignore_missing_imports = True
3,4行目がnumpyに対して、型ヒントチェックのエラーを無視するような設定になる。
他のサードパーティモジュールにも適用したい場合は3,4行目をコピペし、numpy
の部分を変えればよい。
その他、mypy.iniに関する仕様は公式のこちらのページを参照してほしい。
これでmypyチェックを正常に走らすことができる。しかし、ndarray自体の型ヒントチェックも無視されるので注意(最後の行)。
# ex4.py (ignore_missing_imports)
from typing import List, Tuple
import numpy as np
def calc_center(points: List[Tuple[int, int]]) -> Tuple[float, float]:
'''点のリストから重心を求める'''
n = len(points)
x, y = 0, 0
for p in points:
x += p[0]
y += p[1]
return x/n, y/n
def calc_center_np(points: np.ndarray) -> np.ndarray:
'''点のリストから重心を求める(ndarray版)'''
return np.average(points, axis=0)
points = [(1, 1), (4, 2), (3, 6), (-1, 3)]
print(calc_center(points)) # Success
np_points = np.array(points, dtype=np.int)
print(calc_center_np(np_points)) # Success
print(calc_center_np(points)) # Success ?
>mypy ex4.py
Success: no issues found in 1 source file
方法2. 型ヒント用スタブを作成
使いたいモジュールの型ヒント用の空っぽの関数(スタブ)を作成することで、mypyが代わりにそれらを見てくれる。スタブファイルは.pyiという拡張子で管理されている。
githubにnumpy用のスタブnumpy-stubsが公開されていて利用できる。
まずgit clone https://github.com/numpy/numpy-stubs.git
等で"numpy-stubs"フォルダを持ってくる。
"numpy-stubs"フォルダを"numpy"に変更する。
フォルダ構成としては以下のようになる。
numpy-stubs/
└── numpy
├── __init__.pyi
└── core
├── numeric.pyi
├── numerictypes.pyi
├── _internal.pyi
└── __init__.pyi
さらに、MYPYPATH
という環境変数にスタブが置かれているルートフォルダパスを追加して実行する。
# ex5.py (numpy-stubs)
from typing import List, Tuple
import numpy as np
def calc_center(points: List[Tuple[int, int]]) -> Tuple[float, float]:
'''点のリストから重心を求める'''
n = len(points)
x, y = 0, 0
for p in points:
x += p[0]
y += p[1]
return x/n, y/n
def calc_center_np(points: np.ndarray) -> np.ndarray:
'''点のリストから重心を求める(ndarray版)'''
return np.average(points, axis=0)
points = [(1, 1), (4, 2), (3, 6), (-1, 3)]
print(calc_center(points)) # Success
np_points = np.array(points, dtype=np.int)
np_points_float = np.array(points, dtype=np.float)
print(calc_center_np(np_points)) # Success
print(calc_center_np(np_points_float)) # Success
print(calc_center_np(points)) # TypeHint Error
>set "MYPYPATH=numpy-stubs"
>mypy ex5.py
ex5.py:28: error: Argument 1 to "calc_center_np" has incompatible type "List[Tuple[int, int]]"; expected "ndarray"
Found 1 error in 1 file (checked 1 source file)
これで、ndarray自体の型ヒントチェックが機能する。しかし、dtype, shapeの指定した上でのチェックはできない、また、いちいち環境変数を設定しなくてはいけないのが若干ネックである。
stubgen
mypyにはstubgenというスクリプトがついており、自動的に型ヒント用のファイル(.pyi拡張子)を生成してくれる。
>stubgen -p numpy
-p
はパッケージ用に再帰的にスタブを生成するオプションである。
実行するとカレントディレクトリにout
フォルダが生成されておりその中にnumpyのスタブファイルが詰まっている。
しかし、stubgenがうまくnumpyの構造を抽出できてないせいか、mypyチェックを実行すると別のエラーが出てしまう。numpy-stubsのようにスタブが有志で公開されてるケースもあるので、できればそちらを使う方が無難である。
方法3. nptypingも使う
方法1., 方法2.のどちらかを取ったうえで、ndarrayのdtype, shapeを含めた型ヒントを組みたいというのであれば、nptypingを用いるとよい。
PyPiからpip install nptyping
でインストールできる。
nptypingはmypyによる静的な型ヒントチェックには対応していないものの、ndarrayのdtype, shapeを指定した型ヒントをArray
というエイリアスを用いて指定できる。
以下は公式のサンプル。pandasのDataFrameのような型が入り混じった配列もOK。
from nptyping import Array
Array[str, 3, 2] # 3 rows and 2 columns
Array[str, 3] # 3 rows and an undefined number of columns
Array[str, 3, ...] # 3 rows and an undefined number of columns
Array[str, ..., 2] # an undefined number of rows and 2 columns
Array[int, float, str] # int, float and str on columns 1, 2 and 3 resp.
Array[int, float, str, ...] # int, float and str on columns 1, 2 and 3 resp.
Array[int, float, str, 3] # int, float and str on columns 1, 2 and 3 resp. and with 3 rows
isinstance
を用いたインスタンスチェックも可能である。
# ex6.py (nptyping)
from typing import List, Tuple
import numpy as np
from nptyping import Array
def calc_center(points: List[Tuple[int, int]]) -> Tuple[float, float]:
'''点のリストから重心を求める'''
n = len(points)
x, y = 0, 0
for p in points:
x += p[0]
y += p[1]
return x/n, y/n
def calc_center_np(points: Array[int, ..., 2]) -> Array[float, 2]:
'''点のリストから重心を求める(ndarray版)'''
print(isinstance(points, Array[int, ..., 2]))
return np.average(points, axis=0)
points = [(1, 1), (4, 2), (3, 6), (-1, 3)]
np_points = np.array(points, dtype=np.int)
np_points_float = np.array(points, dtype=np.float)
print(isinstance(calc_center_np(np_points), Array[float, 2])) # 引数: True, 戻り値: True
print(isinstance(calc_center_np(np_points_float), Array[float, 2])) # 引数: False, 戻り値: True
print(isinstance(calc_center_np(points), Array[float, 2])) # 引数: False, 戻り値: True
mypy.iniでnptypingをignore_missing_imports = True
に設定するのを忘れずに。
実行結果は以下。
>mypy ex6.py
Success: no issues found in 1 source file
>python ex6.py
True
True
False
True
False
True
まとめ
numpyまわりの型ヒントについてざっくりまとめた。
座標・表データなどの情報をndarrayとして扱い、幾何的・統計的な演算を実装することはよくあると思う。
その際に「何次元のndarrayをこねくり回してるのか?」などと自分が書いたコードでもよく悩んだりする。
可読性や保守性も踏まえてnptypingのような型ヒントは有用だと思った。
将来的にmypyによる型チェックにも対応できるとより有用性が高まるのではないかと思う。
参考記事
https://stackoverflow.com/questions/52839427/
https://www.sambaiz.net/article/188/
https://masahito.hatenablog.com/entry/2017/01/08/113343