Pythonとmypyで型ヒントを利用する(基礎編)
概要
Python3.5から、型ヒントがサポートされるようになりました。
これらは、Pythonに変数の型チェック機能をもたらすものです。 変数に型チェック機能があることで、プログラムをより安全に記述できるようになります。 また、コードを見ただけでその変数にどのようなクラスや型の値が入っているのかが一目瞭然になるので、ドキュメンテーションとしてもその真価を発揮します。
さて、実はPython3が持つこの型ヒントは、あくまでヒントであって実行時には何の役にも立ちません。 ではどうするかというと、mypyというツールを使って型の状態を静的にチェックします。
なお、今回はPython3.6以降を想定しています。 というのも、通常の変数の型ヒントが、関数の引数同様のフォーマットで行えるようになったためです。楽ちんです。
準備
mypyをインストールするだけです。pipでインストールしてしまいましょう。
pip install mypy
試してみる
型ヒントの指定自体は非常に簡単です。
基本的に変数名の右側に: 型名
と記述するだけです。
関数やメソッドの戻り値の型を指定するときには、引数のカッコ()
の後に->型名
と指定します。
実際に全て使ったサンプルは以下です。
def test(x: int) -> str:
return "int to string {}".format(x)
result: str = test(123)
非常にシンプルですね!
関数test
はint型
の値を引数に取り、str型
の値を返します。
そして変数result
はstr型
の値を持つ、となっています。
さて、すでに述べた通りPythonの型ヒントは記述するだけでは意味がありません。 実際にmypyを使って型チェックしてみましょう!
[koji:work]$ mypy test4.py
[koji:work]$
問題がないので何も表示されません!完璧なコードです! (ファイル名は適当に読み替えてください。)
エラーを出してみる
さて、では異なる値が渡された場合などはどうなるのでしょうか? 上記のコードを以下のように修正します。
# -> Noneを指定すると、戻り値なし(returnなし)が期待される
def test(x: int) -> None:
return "int to string {}".format(x)
result: int = test("123")
これをmypyでチェックしてみると、
[koji:work]$ mypy test4.py
test4.py:2: error: No return value expected
test4.py:4: error: Argument 1 to "test" has incompatible type "str"; expected "int"
test4.py:4: error: "test" does not return a value
[koji:work]$
ちゃんとおかしな部分が検出されました!
関数test
は本来returnが無いはず(戻り値が->Noneになっているので)なのに、return文があるのでその部分がエラーになります。
そして、2つ目は、引数はint型を期待しているのに、実際に渡されているのは文字列の"123"
なので、当然型が合わないのでエラーです。
最後に、result
という変数にtest()
の実行結果を代入しようとしているけど、そもそもtest()
関数は戻り値が無いよ(->Noneになっているので)とのエラーになっています。
Javaなどのコンパイラを利用する静的型付け言語などと全く同じような事がPython本体の型ヒント機能とmypyを利用することで実現できました!
これだけ短いコードだと中々メリットが実感できないと思いますが、Pythonの型ヒントとmypyを利用することでそもそもプログラム実行前にこのエラー群を事前にキャッチできますので、test()
の中でもし引数が意図した型じゃない場合の処理を記述する必要がなくなります。
私のPython力がまだまだ低いので間違っているかもしれませんが、もし型ヒントを用いずに引数の型が意図したものかどうかをチェックするとなるとtype()
関数や、hasattr()
関数を使って、自分のコーディングしている関数内で、引数の妥当性をチェックする必要が出てきます。
しかし型ヒントとmypyを利用すれば、そもそも引数の型が正しく意図したものであることが保証されるので、その関数の中で安心して引数を利用することが出来ます。
そしてコードをぱっと見ただけでどのような値が利用されているかが解りやすくなるのでドキュメントの側面も無視できません。(個人的にはこっちのほうがメリットが大きい)
自作クラスを使ってみる
さて、当然int
やstr
と言った組み込みの型以外にも、自分で定義したクラスも型ヒントとして利用できます。
先ずは実際に動作するコード。
class User:
def __init__(self, name: str) -> None:
self.name: str = name
def get_name(self) -> str:
return self.name
def hello(user: User) -> str:
# 渡された引数はUser型であることが保証されているので、.get_nameメソッドの存在チェックをする必要がない!
return "Hello! {}".format(user.get_name())
obj: User = User('koji')
assert( hello(obj) == "Hello! koji" )
mypyを実行してもエラーは出ません。
[koji:work]$ mypy test4.py
[koji:work]$
素晴らしいですね! では実際に異なるクラスを渡すとどうなるか見てみましょう。
class User:
def __init__(self, name: str) -> None:
self.name: str = name
def get_name(self) -> str:
return self.name
class Hoge:
def __init__(self, name: str) -> None:
self.name: str = name
pass
def hello(user: User) -> str:
# 渡された引数はUser型であることが保証されているので、.get_nameメソッドの存在チェックをする必要がない
return "Hello! {}".format(user.get_name())
obj: User = Hoge('koji')
assert( hello(obj) == "Hello! koji" )
mypyでチェックすると。。。?
[koji:work]$ mypy test4.py
test4.py:18: error: Incompatible types in assignment (expression has type "Hoge", variable has type "User")
[koji:work]$
ちゃんとエラーが出力されました! なお、すでに述べたように型ヒントはあくまでヒントで、プログラムの実行時点ではまったく考慮されません。 そのため、上記のmypyではエラーになるコードをpythonで実行すると以下のようになります。
[koji:work]$ python test4.py
Traceback (most recent call last):
File "test4.py", line 19, in <module>
assert( hello(obj) == "Hello! koji" )
File "test4.py", line 15, in hello
return "Hello! {}".format(user.get_name())
AttributeError: 'Hoge' object has no attribute 'get_name'
[koji:work]$
結局エラーになるからmypy使わなくてもいいんじゃないの?と思われるかもしれません。 実際小さなこの程度のコード量ならそれでも問題ありませんが、クラスが数十から数百というレベルになってくると、全てのコードに引数の妥当性をチェックするコードを書くのはかなりの負担になります。 また、どのような値が渡されるかは変数名から類推するしかありません。 その点、型ヒントとmypyを使っておけば、渡されるコードの妥当性、そして後々の保守の際に即座に変数の値がどの型なのか把握できます。
注意点
mypyを使っていて2点、気になった部分があるのでココに纏めます。
関数の中身をチェックしたい場合
以下のような関数は、実はmypyによってチェックされません。
def abc():
b: str = 1
str型
の変数b
にint型の1を代入しているので、本来であれば型が合わない旨のエラーをmypyが教えてくれそうですが、実はそうはなりません。
[koji:work]$ mypy test5.py
[koji:work]$
なぜかというと、この関数abcの宣言が型ヒントを利用していないためです。 そのため、mypyはこの関数abcをチェックしてくれていない、という状態です。 つまりmypyは、関数宣言で型ヒントの無いものについてはその中身はチェックしない、という動作になっています。
ではどのようにすればいいかというと、単純に型ヒントを付けるだけです。
def abc() -> None: # 何もreturnしないのでNoneを付ける
b: str = 1
# 型ヒントがあれば良いので、別に使わない引数を渡すようにしてもチェックされるようになる
# def abc(x: str):
# b: str = 1
これを実行すると
[koji:work]$ mypy test5.py
test5.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")
[koji:work]$
という感じでしっかりmypyが型に関する情報をチェックしてくれるようになったことが確認できます。
この動作は、既存のコードに型ヒントを導入する際に役立ちます。 例えば既存のモジュールやクラスに新しい関数/メソッドを追加する際に、その新しい追加部分だけ型ヒントを付ける、ということが実現できます。 もしmypyが無条件に全関数をチェックしてしまうと、自分が中身を理解できない別の既存関数の引数や戻り値、そして関数の処理にまで型ヒントを強制されることになってしまいます。 このように、型ヒントが指定された関数だけmypyがチェックしてくれることにより、無理なく既存コードに型ヒントを付け、mypyを利用して段階的に安全なコードに近づけていくことが可能になっています。
Mixing dynamic and static typing
なお、上記にはプロトタイプを作る際には型ヒント無しで作っておいて、ある程度機能が固まったタイミングで型ヒントを導入することも出来るよ、というメリットも説明されています。
ただし、この注意点についてはあくまで関数/メソッドの話です。 モジュールのトップレベルに宣言した変数の場合、型ヒントがあれば無条件でmypyがチェックします。
# すでに述べた通りこの関数宣言自体に型ヒントが無いのでmypyは無視する
def abc():
b: str = 1
# これはモジュールのトップレベルに宣言されているのでmypyがチェックしてエラーを出す
b: str = 1
これを実行すれば、以下のように関数abcは無視されて、最後の1行(モジュールのトップレベル)の部分のみmypyにチェックされていることが分かります。
[koji:work]$ mypy test5.py
test5.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str")
[koji:work]$
型ヒントの無い関数を利用する場合
型ヒントが無い関数を利用する際の動作です。 以下をmypyで実行してもエラーは出ません。
def test1():
return 100
def test2() -> None:
v: str = test1() # intが返るけど、test1()はた型ヒントが無いのでココはok
print(v)
v: str = test1() # intが返るけど、test1()はた型ヒントが無いのでココはok
test2()
[koji:work]$ mypy test6.py
[koji:work]$
戻り値があるけど型ヒントの無い関数の場合、戻り値はtyping.Any
型が返るのと同等の扱いになるようです。(恐らくmypyのドキュメントに記述があるはずだけど見つけられない。。。)
Any型は、なんかの型だけど何でもイイ、というようなワイルドカード的な特殊な型です。
なので、Anyをreturnするようにしても同様の実行結果になり、mypyは型が合わない旨のエラーを検知してくれません。
from typing import Any
def test1() -> Any:
return 100
def test2() -> None:
# この関数(test2)自体は型チェックの対象だけど、test1の戻り値はAny扱いで何でもOKになる。
v: str = test1()
print(v)
#この変数v自体は型チェックの対象だけど、test1の戻り値はAny扱いで何でもOKになる。
v: str = test1()
test2()
[koji:work]$ mypy test3.py
[koji:work]$
test1の戻り値をAny以外の正しい値で指定すればちゃんとmypyが型エラーを検知してくれます。
from typing import Any
def test1() -> int:
return 100
def test2() -> None:
v: str = test1() # Anyじゃなくてintが返るのでエラーにできる
print(v)
v: str = test1() # Anyじゃなくてintが返るのでエラーにできる
test2()
[koji:work]$ mypy test6.py
test6.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "str")
test6.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")
[koji:work]$
型ヒントが無い関数はmypyは暗黙的にAnyを返していると判断していて、それを利用する別の関数が型ヒントを利用している場合型エラーが出ない、これは既存の型ヒントが導入されていないライブラリを利用する側の事を考えての動作だと思います。
変数への再代入について
型ヒントが付いた変数に値を再代入する際の動作です。
同じ型の値を再代入
特にmypyはエラーを出しません。型が同じですもんね。
a: str = "hoge"
a = "piyo"
異なる型の値を再代入
これはmypyがちゃんとエラーを検出してくれます。
a: str = "hoge"
a = 1
[koji:work]$ mypy test4.py
test4.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")
異なる型で再宣言して代入
型が異なるのが問題なのであれば、異なる型ヒントを付けて変数を再宣言すればどうでしょう?(Python的に変数の再宣言という言い方が正しいのかは別として)
a: str = "hoge"
a: int = 1
これをmypyで実行すると以下のような今までと違うエラーが出力されます。
[koji:work]$ mypy test4.py
test4.py:2: error: Name 'a' already defined
これはつまり変数a
が2回宣言されたよ、という意味です。
なので、2回めの変数a
の宣言を1度目と同じ型ヒントでa: str = ...
としても同様のエラーになります。
これはつまり、変数の宣言時に必ず型ヒントを付けるようにすれば、プログラム実行前に変数への再代入をmypyが検知してくれるということになります。つまり本物の定数が実現できます。 当然、変数宣言に型ヒントを付けること自体忘れてしまうと、再代入しようとして同じ型の場合は問題なくmypyを通ってしまうので、コードレビューやLINT系ツールなどを利用することが必須となりますが。。。
まとめ
いかがでしょうか。 駆け足でざっくりだったのでまだ良くわからない部分もあるかと思います。 もっと利用できる既存の型や、外部ライブラリを利用する際に注意する点など別途また書いていきます。
記事を読んで頂ければ何となく解ると思いますが、ちょっと気をつけないと、mypyでチェックしているつもりが実はチェックできていなかった、ということが起こりかねません。 それを防ぐには、少なくとも自分が書いた新しいコード部分に関しては必ず型ヒントを付けるようにする、という事を心がけるしか無いかな、と思います。 この辺りLINT系ツールやなんかで代用できる気もしますが、先ずは自分で型ヒントを付けていくことに慣れていけば良いのかな?と思います。
そもそも型ヒント自体はPythonの実行自体には影響を及ぼさないものなので、何も考えずに今後はどんどん型ヒントを付けてしまったほうがいいのでは?と思います。(当然嘘になっちゃうとダメですけどそれはコメントと同じですしね!)
公開日:2018/03/16