Blank File

LinuxとかPythonとかVimとか、趣味でいじる感じで

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氏の提案に賛同しており *1Pythonの仕様が型アノテーションをどこまで正式にサポート(あるいは強制)するのかに関わらず、これらのツールは今後この方針で開発が進められる *2 だろうと考えられます。

本記事では、Guido氏が推しているmypy方式も含めて、Pythonでの型を使ったエラーチェック・コード補完をいくつか試してみます。

以下のIDEツールで試しました。

  • エラーチェック
    • 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) とすると警告が出ました。

f:id:h-miyako:20141111051138j:plain

pyflakes, mypy, jedi-linter

同じコードで試しましたが、警告は出ませんでした。

補完

PyCharm

関数の戻り値の型がstrと指定されているので、補完候補にはstrメソッドだけが表示されています。

f:id:h-miyako:20141111051552j:plain

repeat関数の引数に誤った型 (両方int) を入れても、docstringの記述を優先して返り値はstrと判断しているようです。

f:id:h-miyako:20141111051606j:plain

ちなみに、このくらいの簡単な関数だとDocstringなしでも適切な補完候補を出してくれました。

f:id:h-miyako:20141111051659j:plain

Jedi

Jediでも返り値がstrという所はdocstringから判定しているのか、strメソッドだけが補完候補に表示されています。

f:id:h-miyako:20141111051723j:plain

型指定を無視してx, y 両方にintを入れる(返り値はintになる)と、Jediはstrintの両方の補完候補を出しました。 このあたりはPyCharmとは違う動作ですね。

f:id:h-miyako:20141111051829j:plain

また、DocstringなしではJediもPyCharmと同じようにstrメソッドだけが表示されました。 このくらいの単純なコードでは、解析して補完候補を出しているようです。

f:id:h-miyako:20141111051846j:plain

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は両方エラーを出しました。

f:id:h-miyako:20141111052032p:plain

f:id:h-miyako:20141111052040p:plain

一方、他のツールは全くエラーを出しませんでした。 この記法(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に書かなくても、不適切な型を引数に与えた時は警告を出してくれます。

f:id:h-miyako:20141111052303j:plain

Mypy

型がおかしいと警告をだしてくれました。

f:id:h-miyako:20141111052333j:plain

Pyflakes, Jedi-linter

エラー検出なし

補完

PyCharm

Docstringで型を指定した場合と同様、指定された通りの型が返ると判断してstrメソッドを候補に出してきます。

f:id:h-miyako:20141111052403j:plain

Jedi

アノテーションは完全に無視し、コードの静的解析結果だけで返り値を判断して補完候補を決めているようです。

以下、repeat('a', 3) の場合は返り値がstrなのでstrメソッドだけを表示し、repeat(1, 3)の場合は返り値がintなのでintメソッドだけを表示しています。

f:id:h-miyako:20141111052443j:plain

f:id:h-miyako:20141111052448j:plain

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])を試します。 ListDict は標準の型ではなく、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だけがエラーを検出しました。 エラーメッセージの内容も型の違いを明示してくれていい感じです。

f:id:h-miyako:20141111052753p:plain

Mypyのtypingモジュールには他にもIterableAny など色々とありますが、キリがないのでここまでにします。 詳細は mypyのExamplesTutorial を参照してください。

指定無しでの型チェック(型推論

冒頭のGuido氏の提案メールのやりとりを眺めていた所、 Jediがtypeチェックをするlinterを開発中という話 が出ていました。 型が示されていなくても、型を推測して不適切な演算やメソッドが呼ばれる時に警告を出すようです。

以下がメール中で例示されていたコードです。

def foo(x):
    return x + "foo"

foo(1)

実行すると intstr の足し算になってエラーで落ちます。 jedi linterではこのエラーが検出できるそうです。

エラーチェック

Jedi-linter

実行方法は python -m jedi linter [filename] です。 確かにエラー扱いになっていますね。 (エラーにすべき場所は 7行目のfoo(1) がベターだとは思いますが、開発中なので今後に期待)

f:id:h-miyako:20141111053005j:plain

Jediで返り値に対する補完候補を表示するとintstrが混在しているので、型が上手く判定できていないようです。

f:id:h-miyako:20141111053044j:plain

PyCharm

PyCharmでは上記コードはエラーになりませんでした。

f:id:h-miyako:20141111053244j:plain

直接 intstr を足す単純なコード (a = 1 + 'a') や、一旦変数に代入するくらいの簡単なコード(b = 'a'; c = b + 1)では type error を出してくれました。

f:id:h-miyako:20141111053251j:plain

f:id:h-miyako:20141111053310p:plain

Mypy

PyCharmと同じく、関数を使った場合にはエラーが検出できず、直接 a = 1 +'a' などとした場合はエラーを検出出来ました。

f:id:h-miyako:20141111053330p:plain

Pyflakes

エラーになりませんでした。

まとめ

今回試したツールIDEの対応状況を以下にまとめます。

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は今回初めて使ったのですが、かなり使いやすいです。 EclipsePython拡張を入れるくらいならPyCharm使ったほうがいいです。


今回、何通りかPythonで型を扱う方式を紹介してきました。 現時点でも、上手くツールを使うことで、型情報の助けを得ながらPythonのコードを書くことは十分できそうです。

私は普段Vimでコードを書いているので、Python3に完全に移行できるまではJediの補完 + Pyflakes と jedi-linter のエラーチェックという形にしようかな、と思っています。 Python3に移行したら、関数アノテーションとmypyを積極的に使っていきたいですね。 個人的な願望を言うと、関数アノテーションが流行ることでPython3への移行が促進されたりするといいんですけどね・・・

あと、PyCharmは本当に良かったので、今後少しずつ使い方を見ていくつもりです。

補足

実行環境

今回大量に貼りつけたスクリーンショットのうち、白背景はPyCharm、黒背景はVimです。

Jedi, mypy, pyflakes はVimvim-watchdogs プラグインを使って実行とエラー箇所のハイライトを行っています。 vim-watchdogsの設定方法は 前回の記事 のとおりです。 jediの補完は jedi-vimneocomplete.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の補完やエラーチェックだとropepylintが有名かと思います。 これらは今回試していないのでノーコメントです。

他の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氏の提案に含まれています。

*4:Type Hinting in PyCharm