魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

Python で型チェックする

概要

型ヒント をちょっと(ちょっとだけ)触ったのでメモ程度にまとめます

docs.python.org

ちゃんと理解したい人は以下のドキュメントを読んだ方が早いと思います。

www.python.org

型ヒント

Python は動的型付け言語なので、変数の型を宣言する必要がありません。 実行時にその変数の使われ方からその場その場で型を解釈してくれます。

似た言葉として 型推論 というものもありますが、動的型付けと型推論は別物です。 型推論は静的型付け言語において、つまり初めから型が確定していないといけない言語において、コード内で明示的に型を宣言していなくてもコンパイル時に型を推論してくれる機能です。

型ヒントの書き方ですが、まず関数を定義するときに引数と戻り値の型を書くものがあります。

def fuga(a:int, b: str) -> dict:
    return {a: b}

こういう感じです。 変数名; 変数の型-> 戻り値の型 を書きます。戻り値がない場合は -> None ですね。 これ自体は何も import しなくても書けます。 ただし、この関数を次のように呼び出しても( Python という言語そのものの仕様のため)特にエラーにはならないので注意が必要です。

fuga('a', 'b')

クラスを定義するときは self の型って何になるんだろうと思ったらこの場合は書かないみたいです。 まぁ仕組み上間違えることはないですからね。

class Animal:
    def __init__(self, __name: str) -> None:
        self.__name = __name

    def greet(self) -> str:
        return self.__name

変数の宣言もできます。

def fuga(a:int, b: str) -> dict:
    c: int
    c = 2
    return {a + c: b}

普通 Python には変数の宣言はなく、いきなり代入から始まります。 上の例でいうと普通はいきなり以下のように書きますよね。

c = 2

また、

c : int = 2

のような書き方もできます。

ただ、型が dict だと宣言していてもキーと値の型が想定したものになっているかはまた別の話として存在します。

標準ライブラリの typing を import します。 先ほどの例はこのようになります。

from typing import Dict

def fuga(a:int, b: str) -> Dict[int, str]:
    c: int
    c = 2
    return {a + c: b}

Dict[キーの型, 値の型] となります。 list 型の場合は List[要素の型] となりますが list 型って複数の型を入れられるのでその場合はどう表現したら良いんですかね? List[int, srt] はエラーになる( [] に入れられるのは一つだけ)のでその場合はどうするんですかね?

エイリアス

それなりに複雑な構造を持った型を変数として何回も使う場合、毎回書いてると見通しがよくありませんし間違いの元にもなります。 そんな場合は型エイリアスを使います。

from typing import Dict

Book = Dict[int, str]

def hoge(page: int, book: Book) -> str:
    return book[int]

めちゃくちゃ意味のない関数ですがこのような感じに型にエイリアスを張ることができます。 このエイリアスを使って作った型にまたエイリアスを張って…ということもできるので良い感じにできそうですね。

mypy

Python の型について静的解析するツールが mypy です。

github.com

pip installpipenv install なりして使います。後述しますがスクリプト実行時は特に何もしないため、本番環境へのデプロイを前提としたプロジェクトでは(Pipenv のやり方で言えば) pipenv install mypy --dev のようにして開発用パッケージとして本番環境にはインストールしないようにするのが良いかと思います。 flake8 のようなリントツールや pytest のようなテスト用ライブラリの時と一緒ですね。

使い方は README を見ていただければわかるように簡単です。 チェックしたい対象に対して

$ mypy hoge.py
Success: no issues found in 1 source file

のように実行するだけです。 出力で解析結果が得られます。 関数における型の定義、関数の使い方についてちゃんと宣言した通りの型になっているかをチェックしてくれます。

def func2(a: int, b: str) -> List[int]:
    return [a, b]

こんなものを用意します。 そして mypy コマンドを実行するとこんな感じで結果が帰ってきます。

$ mypy src/
src/main.py:30: error: List item 1 has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 2 source files)

これは宣言の仕方が間違ってるよ、というものですね

次はこんなのを用意します。 関数 fuga の第一引数は int だと宣言しているの最後の行で str 型を渡しています。 もちろん dict のキーに str 型は使えますので実行時にエラーは起きませんが、意図した使い方ではないということになります。

def fuga(a:int, b: str) -> Dict[int, str]:
    c: int
    c = 2
    return {a + c: b}

print(fuga('a', 'b'))

mypy コマンドを叩きます。

$ mypy src/
src/main.py:26: error: Argument 1 to "fuga" has incompatible type "str"; expected "int"
src/main.py:32: error: Name 'func2' is not defined
Found 2 errors in 1 file (checked 2 source files)

Argument 1 to "fuga" has incompatible type "str"; expected "int" って教えてくれてますね。 あと、先ほどの例で使った関数 func2 の定義をコメントアウトして実行したのに func2 を呼び出している箇所をコメントアウトし忘れていたので、そこのミスも検出してくれてますね。 型のチェックだけかと思っていましたがこの辺もチェックしてくれるみたいです。 そしてコメントアウトしたところはチェック対象外にしてくれるんですね。

ちなみにこの mypy コマンドを実行すると、実行した階層に .mypy_cache という隠しディレクトリが作成されます。 中身はこんなのです。自動的に .gitignore が生成されてそこに * と書かれているのでコミット対象には入らないようにしてくれています。

$ tree -a .mypy_cache 
.mypy_cache
├── .gitignore
└── 3.8
    ├── @plugins_snapshot.json
    ├── _ast.data.json
    ├── _ast.meta.json
    ├── _importlib_modulespec.data.json
    ├── _importlib_modulespec.meta.json
    ├── abc.data.json
    ├── abc.meta.json
    ├── ast.data.json
    ├── ast.meta.json
    ├── builtins.data.json
    ├── builtins.meta.json
    ├── codecs.data.json
    ├── codecs.meta.json
    ├── collections
    │   ├── __init__.data.json
    │   ├── __init__.meta.json
    │   ├── abc.data.json
    │   └── abc.meta.json
    ├── genericpath.data.json
    ├── genericpath.meta.json
    ├── generics.data.json
    ├── generics.meta.json
    ├── importlib
    │   ├── __init__.data.json
    │   ├── __init__.meta.json
    │   ├── abc.data.json
    │   └── abc.meta.json
    ├── io.data.json
    ├── io.meta.json
    ├── mmap.data.json
    ├── mmap.meta.json
    ├── os
    │   ├── __init__.data.json
    │   ├── __init__.meta.json
    │   ├── path.data.json
    │   └── path.meta.json
    ├── posix.data.json
    ├── posix.meta.json
    ├── sys.data.json
    ├── sys.meta.json
    ├── types.data.json
    ├── types.meta.json
    ├── typing.data.json
    └── typing.meta.json

4 directories, 42 files

json の中身が気になる型は一度ご自身で試してみてください。そっ閉じ系のやつです(つまり見てもわからん)。

その他

NewType とかジェネリクスとかいろいろまだよくわかってないのでもう少し遊んでから改めてまとめたいと思います。