Pythonと型 〜 Docstring、関数アノテーション、型推論 〜
動的型付言語であえて型を宣言する方式(Type Hinting or Optional Typing)、最近流行ってますね。 言語仕様としては Dart や TypeScript が採用していますし、Pythonでは 開発者のGuido氏が次期バージョン (3.5) での型アノテーションの導入を提案 しています。
Pythonで型アノテーションの導入って結構インパクトありそうですが、国内の情報がほとんどなかったので、実際どんなもんなのか、既存ツールで試せる範囲で試してみました。
そもそも、Python で型を使うと何が嬉しいのか、 Guido氏の提案メール から引用します。
- Editors (IDEs) can benefit from type annotations
- Linters are an important tool for teams developing software
- Type annotations are useful for the human reader as well!
- Refactoring
- Optimizers (like PyPy or Pyston)
五つ目の、型に基づいて最適化するPython実行系とか出てきたら面白いですね。 残念ながら、これはまだ将来の話になりそうですが。
すぐに効果が実感できそう (あるいは、すでに試せる状態) なのは上の一つ目と二つ目でしょうか。 一つ目は型に基づいた補完候補の表示やミスの発見についてで、二つ目も型によるエラーの発見です。 これらの機能はすでに IDE や 各種ツール (Mypy, jediなど) で提供されています。 さらに、IDE (PyCharm) の開発者やjedi、mypyの開発者もGuido氏の提案に賛同しており *1 、Pythonの仕様が型アノテーションをどこまで正式にサポート(あるいは強制)するのかに関わらず、これらのツールは今後この方針で開発が進められる *2 だろうと考えられます。
本記事では、Guido氏が推しているmypy方式も含めて、Pythonでの型を使ったエラーチェック・コード補完をいくつか試してみます。
- エラーチェック
- PyCharm (community edition)
- Mypy
- Jedi-linter (
python -m jedi linter filename.py
) - pyflakes
- 補完
- PyCharm (community edition)
- Jedi
また、今回試していない実装方法も複数存在します。 Lukasz氏がまとめていらっしゃる ので、興味のある方は一読してみるといいかもしれません。 特にGoogleプレゼンツなpytypedeclは型情報を別ファイルに用意するという方式で、今回試したどの実装とも異なります。 (個人的には「面倒臭そう」という印象が先に立ったので試しませんでした)
Docstring
完全にコメントベースの方式です。 したがってPythonのバージョンに依存しません。 執筆時点ではPyCharm が対応しています。 Jediの補完も公式に対応しているようです。
記法など詳細は Python - PyCharm での type hinting を使いこなす - Qiita を参照してください。
エラーチェック
PyCharm
上記Qiitaの投稿と同じコードを、まず PyCharm で試してみます。
def repeat(x, y): """ :param str x: value to repeat :type y: int :param y: number of repeat :rtype: str :return: repeated string """ return x * y
repeat
関数の第一引数(x
)がstr
、第二引数(y
)がint
で、返り値はstr
と指定しています。
ここで、str
を指定している所(x
)に数値(int
)を入れ、c = repeat(1, 3)
とすると警告が出ました。
pyflakes, mypy, jedi-linter
同じコードで試しましたが、警告は出ませんでした。
補完
PyCharm
関数の戻り値の型がstr
と指定されているので、補完候補にはstr
のメソッドだけが表示されています。
repeat関数の引数に誤った型 (両方int
) を入れても、docstringの記述を優先して返り値はstr
と判断しているようです。
ちなみに、このくらいの簡単な関数だとDocstringなしでも適切な補完候補を出してくれました。
Jedi
Jediでも返り値がstr
という所はdocstringから判定しているのか、str
のメソッドだけが補完候補に表示されています。
型指定を無視してx
, y
両方にint
を入れる(返り値はint
になる)と、Jediはstr
とint
の両方の補完候補を出しました。
このあたりはPyCharmとは違う動作ですね。
また、DocstringなしではJediもPyCharmと同じようにstr
のメソッドだけが表示されました。
このくらいの単純なコードでは、解析して補完候補を出しているようです。
List, Dictなど
エラーチェックだけもう少し複雑なコードで試してみます。
Type Hinting in PyCharm を参考に、list[int]
と dict[str, int]
を試してみます。
def conn(list_): """ :type list_: list[str] :rtype : str """ res = '' for l in list_: res += l return res a = conn([1,2,3,4,5]) def conn2(dict_): """ :type dict_: dict[str, int] :rtype : str """ res = '' for k, v in dict_.keys(): res += k + str(v) return res a = conn2({1:'a', 2:'b', 3:'4'})
PyCharmは両方エラーを出しました。
一方、他のツールは全くエラーを出しませんでした。
この記法(list[str]
など)はPyCharmのヘルプから持ってきたものなので、他のツールは対応していないようです。
関数アノテーション
Python 3.0 で導入された関数アノテーション(PEP 3107)で型を指定します。 この記法はPython 2系にバックポートされていないので、Python 3.0以上でしか使えません。
今回はMypyの形式で型指定します。 先ほどと同じコードを、関数アノテーションで型指定すると以下のようになります。
def repeat(x: str, y: int) -> str: return x * y
Docstringと比べると非常にスッキリしていますね。
関数の引数は 変数名: 型
の形式で指定します。
関数の返り値は、:
の前に -> 型
で指定します。
情報が少なすぎる?そう思ったらdocstringを書きましょう。
エラーチェック
PyCharm
上記コードで問題ないのですが、やたらと「Docstringにtype書け」と警告を出してきてちょっとうざいです・・・ Docstringに書かなくても、不適切な型を引数に与えた時は警告を出してくれます。
Mypy
型がおかしいと警告をだしてくれました。
Pyflakes, Jedi-linter
エラー検出なし
補完
PyCharm
Docstringで型を指定した場合と同様、指定された通りの型が返ると判断してstr
のメソッドを候補に出してきます。
Jedi
型アノテーションは完全に無視し、コードの静的解析結果だけで返り値を判断して補完候補を決めているようです。
以下、repeat('a', 3)
の場合は返り値がstr
なのでstr
のメソッドだけを表示し、repeat(1, 3)
の場合は返り値がint
なのでint
のメソッドだけを表示しています。
List, Dictなど
Docstringと同様、ListとDictも試してみます。 まずはPyCharmの記法で。
def conn(list_: list[str]) -> str: res = '' for l in list_: res += l return res a = conn([1,2,3,4,5]) def conn2(dict_: dict[str, int]): res = '' for k, v in dict_.keys(): res += k + str(v) return res a = conn2({1:'a', 2:'b', 3:'4'})
PyCharmもエラーを出してくれません・・・
次はmypyの記法(List[str]
と Dict[str, int]
)を試します。
List
と Dict
は標準の型ではなく、mypyが提供するtyping
モジュールからimportします
*3
。
from typing import List, Dict def conn(list_: List[str]) -> str: res = '' for l in list_: res += l return res a = conn([1, 2, 3, 4, 5]) def conn2(dict_: Dict[str, int]) -> str: res = '' for k, v in dict_.keys(): res += k + str(v) return res b = conn2({1: 'a', 2: 'b', 3: 'c'})
今回はPyCharmはエラーを検出できず、mypyだけがエラーを検出しました。 エラーメッセージの内容も型の違いを明示してくれていい感じです。
Mypyのtyping
モジュールには他にもIterable
や Any
など色々とありますが、キリがないのでここまでにします。
詳細は
mypyのExamples
や
Tutorial
を参照してください。
指定無しでの型チェック(型推論)
冒頭のGuido氏の提案メールのやりとりを眺めていた所、 Jediがtypeチェックをするlinterを開発中という話 が出ていました。 型が示されていなくても、型を推測して不適切な演算やメソッドが呼ばれる時に警告を出すようです。
以下がメール中で例示されていたコードです。
def foo(x): return x + "foo" foo(1)
実行すると int
と str
の足し算になってエラーで落ちます。
jedi linter
ではこのエラーが検出できるそうです。
エラーチェック
Jedi-linter
実行方法は python -m jedi linter [filename]
です。
確かにエラー扱いになっていますね。
(エラーにすべき場所は 7行目のfoo(1)
がベターだとは思いますが、開発中なので今後に期待)
Jediで返り値に対する補完候補を表示するとint
とstr
が混在しているので、型が上手く判定できていないようです。
PyCharm
PyCharmでは上記コードはエラーになりませんでした。
直接 int
と str
を足す単純なコード (a = 1 + 'a'
) や、一旦変数に代入するくらいの簡単なコード(b = 'a'; c = b + 1
)では type error
を出してくれました。
Mypy
PyCharmと同じく、関数を使った場合にはエラーが検出できず、直接 a = 1 +'a'
などとした場合はエラーを検出出来ました。
Pyflakes
エラーになりませんでした。
まとめ
Docstrings | 関数アノテーション | 型推論 | |
---|---|---|---|
PyCharm(エラーチェック) | ◎ ※1 | ○ | △ ※2 |
Jedi (linter) | × | × | ○ |
Mypy | × | ◎ ※1 | △ ※2 |
Pyflakes | × | × | × |
PyCharm(補完) | ◎ ※1 | ○ | ○ |
Jedi (補完) | ○ | ○ | ○ |
※1: list/dict にも対応 ※2: 関数を経由するなど複雑な場合は不可
以下、主観満載ですが、Docstring と関数アノテーションの使い心地の比較です。
Docstrings | 関数アノテーション | |
---|---|---|
見やすさ | △ | ○ |
書きやすさ | △ | ○ |
ツール・IDEの対応 | ◎ | △ |
仕様の安定性 | ○ | △ |
型を明示する場合には、関数アノテーションが見やすさという点でdocstringより秀でているという印象です。
情報が一箇所に集まっているので、必要な情報をあちこち探すことがありません。
また、始めはデフォルト値付き引数の書き方が 変数名: 型 = 値
となることに違和感がありましたが、Go言語の変数定義もこのスタイルなので、徐々に慣れて気にならなくなりました。
一方、関数アノテーションはPython3以上でしか使えないのに対し、docstringは当然Python 2系でも使えます。 したがって、両バージョンへの対応が必要な場合は必然的にdocstringを使うことになると思います。 この方式は標準は定まっていないものの、結構古くからあるらしく、形式も割とまとまっているので *4 、安心して使えそうです。 さらに、PyCharmでのサポートがかなり手厚いので、PyCharmを使えばdocstringを書くことはあまり面倒とは感じないと思います。 PyCharmは今回初めて使ったのですが、かなり使いやすいです。 EclipseにPython拡張を入れるくらいならPyCharm使ったほうがいいです。
今回、何通りかPythonで型を扱う方式を紹介してきました。 現時点でも、上手くツールを使うことで、型情報の助けを得ながらPythonのコードを書くことは十分できそうです。
私は普段Vimでコードを書いているので、Python3に完全に移行できるまではJediの補完 + Pyflakes と jedi-linter のエラーチェックという形にしようかな、と思っています。 Python3に移行したら、関数アノテーションとmypyを積極的に使っていきたいですね。 個人的な願望を言うと、関数アノテーションが流行ることでPython3への移行が促進されたりするといいんですけどね・・・
あと、PyCharmは本当に良かったので、今後少しずつ使い方を見ていくつもりです。
補足
実行環境
今回大量に貼りつけたスクリーンショットのうち、白背景はPyCharm、黒背景はVimです。
Jedi, mypy, pyflakes はVimで vim-watchdogs プラグインを使って実行とエラー箇所のハイライトを行っています。 vim-watchdogsの設定方法は 前回の記事 のとおりです。 jediの補完は jedi-vim と neocomplete.vim を組み合わせています。
pyflakesは、前回の記事で紹介したpython3
で実行する設定を使っています。
Mypyとjedi-linterはwatchdogsに以下の設定を追加し、個別に実行しました。
let g:quickrun_config["watchdogs_checker/mypy"] = { \ "command" : "mypy", \ "exec" : "%c %o %s:p", \ "errorformat" : '%A%f: %m:,%Z%f\, line %l: %m,%f\, line %l: %m', \ } let g:quickrun_config["watchdogs_checker/jedi"] = { \ "command" : "python", \ "cmdopt" : "-m jedi linter", \ "exec" : "%c %o %s:p", \ "errorformat" : '%f:%l:%c: %t%n %m', \ } let g:quickrun_config["watchdogs_checker/jedi3"] = deepcopy(g:quickrun_config["watchdogs_checker/jedi"]) let g:quickrun_config["watchdogs_checker/jedi3"]["command"] = "python3"
上記の設定を.vimrc
に記述し、:WatchdogsRun watchdogs_checker/mypy
や :WatchdogsRun watchdogs_checker/jedi
で実行していました
((長くなったのでさらっと流していますが、Vimの"errorformat"
の設定はかなり苦労しました。Vimの正規表現×謎の%エスケープ辛かったです。バグってる可能性大です。))
。
Mypyは実行に少し時間がかかるので、保存時に毎回実行するような使い方は厳しいかもしれません(watchdogsで非同期にしても)。
jedi linter
の方は実行がかなり速いので、常時チェックをかけてもいいと思います。
ただし、通常のシンタックスチェックはしてくれないので、pyflakesなどと併用する必要があります。
他のツール
Pythonの補完やエラーチェックだとrope
やpylint
が有名かと思います。
これらは今回試していないのでノーコメントです。
他のIDE
一応他のIDEも試(そうとは)しました。
Eclipse + PyDev
設定が悪いのかダメダメな感じでした。 私が使い方をわかっていないからかもしれません。
Eric
コードを貼り付けてもエラーチェックどころかシンタックス・ハイライトもされなかったので試すのを諦めました。
VisualStudio + Python tools
Windows 環境のIDEは試していませんが、実はいい感じなのかもしれません。
*1:[Python-ideas] Proposal: Use mypy syntax for function annotations
*2:すでにPyCharmの開発者とMypyの開発者はgithub上で仕様の策定にとりかかっている
*3:このtypingモジュールを標準モジュールに含めよう、というのが冒頭のGuido氏の提案に含まれています。