これはKLab Advent Calendar 2017の21日目の記事です。
昨年のKLab Advent Calendar 2016でmypyやっていくぞという記事を書きましたが、やっていったのでその続きを書きます。
typingとmypyの更新
去年の時点ではmypy0.4.6でしたが、12月20日の時点でmypyは0.560, pythonは3.6.4がリリースされています。
やっていったプロジェクトではmypyは更新があるたびにバージョンを上げていました。
更新時に目についた機能を紹介します。
mypyはmypyになった
mypyはmypy
というパッケージ名になりました。以前までは名前が衝突していたためmypy-lang
というパッケージ名でした。
pip install mypy
でインストールできます
http://mypy-lang.blogspot.jp/2017/01/mypy-0470-released.html
PEP-526の変数アノテーション
Python3.6からPEP 526が採用され、変数のアノテーション構文が入りました。mypyもこれに対応しています。
変数アノテーション使ってmypyで引っ掛ける例
from typing import ClassVar
# モジュール変数
module_variable: int = "" # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
class Sample:
# クラス変数
class_variable: ClassVar[int]
# デフォルト値付クラス変数
class_variable_default: ClassVar[int] = "" # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
# インスタンス変数
instance_variable: int
def __init__(self) -> None:
self.instance_variable = "" # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
Sample.class_variable = "" # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
typing.NamedTuple
型付き版namedtuple
としてtyping.NamedTuple
がありました、これに変数アノテーションを付ける文法がサポートされています。
NamedTuple
3.6未満だと
from typing import NamedTuple
Sample = NamedTuple('Sample', [('var1', int), ('var2', int)])
と書いていましたが、3.6.1以降だと
from typing import NamedTuple
class Sample(NamedTuple):
"""type annotated namedtuple sample""" # 3.6.1以降だとdocstringsが書ける
var1: int
var2: int
def total(self) -> int: # 3.6.1以降だとメソッドが書ける
return self.var1 + self.var2
このように書けます。ちなみに
3.6以降で変数アノテーションが使え、3.6.1以降でメソッドとdocstringsが使えます。
Protocols
Structural subtyping
というものがtypingでサポートされます。PEP 544
これは既存の継承を使った方法とは別に、クラスに生えるメソッドのシグネチャを制約するもので、
定義されたProtocol
と同じメソッドを持ったクラスを静的解析ツールがProtocol
のサブタイプとみなしてチェックできるようになります。
ランタイムに影響を与えず、静的解析で確認できるところが嬉しいですね。mypyではすでにサポートされています。
言葉だけだと説明が難しいのでIterableを実装する例をあげます。
from typing import Iterable, Iterator
class IntIterable:
def __iter__(self) -> Iterator[int]:
return iter([1, 2, 3])
def consume_iter(iterable: Iterable) -> None:
for i in iterable:
print(i)
consume_iter(IntIterable())
IntIterable
はIterable
を継承していませんが、__iter__
を実装しているためIterable
を引数に取るconsume_iter
に渡すことができます。
また自前でProtocolを定義することもできます。
from typing_extensions import Protocol # 将来的にtypingからインポートできるようになるはず
class Reader(Protocol):
def read(self) -> str:
...
class TextReader:
def read(self) -> str:
return "hoge"
def message_reader(reader: Reader) -> None:
print(reader.read())
message_reader(TextReader()) # OK 明示的にReaderを継承してないけど`read`メソッドを持ってると通る
message_reader(open("somefile")) # OK ビルトインなクラスでもOK
message_reader(1) # Argument 1 to "message_reader" has incompatible type "int"; expected "Reader"
今のところPEP 544 のステータスはDraftですがtyping_extentions
をインストールすると使うことができます。
もちろんmypyでもチェックしてくれます。
http://mypy.readthedocs.io/en/latest/class_basics.html#protocols-and-structural-subtyping
ファイルベースコンフィグ
コマンドライン引数だけではなくファイルで設定を指定できるようになりました。
コマンドラインとは違いモジュール単位で設定できるようになっています。
https://mypy.readthedocs.io/en/latest/config_file.html#the-mypy-configuration-file
どうやっていったか
去年は一旦ビジネスロジックの詰まったサブパッケージにmypyを実行し通るようにしました。
CIに組み込む必要があるなと思いつつも、コードはバリバリと更新されていたので導入するタイミングが難しかったです。
そこで、さらに小分けにして取り込んでいくことにしました。
対象のモジュールを羅列したファイルを指定して実行する方法があったのでこれを採用しました。
https://mypy.readthedocs.io/en/latest/command_line.html#reading-a-list-of-files-from-a-file
mypy_check_filesに
package/module.py
package/sub_package1
package/sub_package2/module.py
このように書き
mypy @mypy_check_files
と実行するとmypy_check_filesに記載されたファイル・ディレクトリを走査して実行してくれます。
これをそのままCIの設定に突っ込んで、mypy_check_filesに対応したファイルを順次追記して取り込んでいきました。
対応できたモジュールから順次取り込めるので、便利ですね。無理にこの仕組を使わなくても、シェルスクリプトでやってもいいと思います。
途中修正したものとか
warn-no-return
return
が無いと怒られるパターンが幾つかあったので修正しました。
def hoge() -> Optional[int]: # 行末にreturnがなくてエラー # error: Missing return statement
if cond1:
return 1
if cond2:
return 2
return
書いたほうが明確でよさそう
--no-warn-no-return
オプション付けると抑制することもできます
error: Return value expected
from typing import Optional
def hoge(s: str) -> Optional[int]:
if s.startswith("1"):
return 1
if s.startswith("2"):
return 2
return # `return expression`と`return`が関数内で混ざってる # error: Return value expected
a = hoge("")
関数中でreturn expression
とreturn
を混ぜるなということらしいです。
https://github.com/python/mypy/issues/1003
PEP8に書いてあるのですが、初めて知りました。
import but not use
去年に引き続きこの問題は残っていますが、
PEP 526とflake8(PyFlakes)の更新でだいぶ楽になりました。
from typing import Iterator # OK
from typing import List # OK
from typing import Dict # マジックコメントでしか使ってないのでflake8エラー # error: F401 'typing.Dict' imported but unused
i: Iterator = iter([])
def f() -> None:
x: List
x = []
print(x)
return None
f()
d = {} # type: Dict
このスクリプトにflake8(3.5.0)を走らせるとDict
が引っかかりまが、PEP 526で対応した部分は通ります。
Sqlalchemy
まだ、対応できてません。
なので、Sqlalchemyのテーブル定義やクエリの詰まったパッケージはチェックしてません。
本体のIssueではpluginでなんとかする方向なのですが、pluginの実装は現在Experimentalです。
参考
https://github.com/python/mypy/pull/3517
https://github.com/python/mypy/issues/3538
行長過ぎる問題
コメント形式・PEP 526形式どちらを付けるにしても、一行あたりの文字数が増えてしまいflake8に怒られるパターンが多々ありました。
今回は気合で改行して乗り切りましたが、もうすこし条件を緩くしても良かったかもしれません。
まとめ
去年に引き続きmypyをやってきましたがtypeshedの不整合は少なくなり、
mypy自体の細かいバグも目に見えるところでは少なくなっていて、日常で使う分には問題なくやっていけると思います。
3.7向けのtyping関連のPEPも多くあり、これからも新しい仕組みが入ってくるでしょう。
新しくプロジェクトを立ち上げる際には導入を検討していいとおもいます。
参考 3.7向けtyping関連PEP
https://www.python.org/dev/peps/pep-0560/
https://www.python.org/dev/peps/pep-0561/
https://www.python.org/dev/peps/pep-0562/
https://www.python.org/dev/peps/pep-0563/
https://www.python.org/dev/peps/pep-0544/
おまけ
dropboxがpyannotateというツールを公開しました
https://github.com/dropbox/pyannotate
http://mypy-lang.blogspot.jp/2017/11/dropbox-releases-pyannotate-auto.html
これは勝手にアノテーション付けてくれる君で、アノテーションついてないコードベースへ導入する際に実行するとコメントベースのアノテーションを付けてくれます。
テストコードなどを使い実際に動かしてランタイムで型を判定する仕組みらしいです。
手元で試したところ、既にアノテーションついている箇所にも追加されてしまいました。
既存のコードベースへタイプアノテーションを導入する際は試してみてもいいと思います。
おまけのおまけ
去年書き忘れてたので言いたかった
typeshedに投げてたPRマージされたよ!!
https://github.com/python/typeshed/pulls?q=is%3Apr+is%3Aclosed+author%3Ak-saka