Pythonではじまる、型のある世界

  • 151
    いいね
  • 0
    コメント

Python3.5からType Hintsという機能が導入されました。

これは型に関する注釈(型アノテーション)をつけることができる仕様で、具体的には以下のような感じになります(Abstractより引用)。

def greeting(name: str) -> str:
    return 'Hello ' + name

アノテーションを実際に行っているのは以下の部分になります。

  • name: str: 引数nameが、str型であることをアノテート
  • -> str: 関数greetingの返り値の型がstrであることをアノテート

また、Type Hintsでは変数宣言における型コメントについても言及されています。

x = []   # type: List[Employee]

こちらは構文ではなく本当にコメントの拡張になりますが、現在既にこうした型に関するコメントを付けているのであれば、上記の記法に乗っ取っておけば将来的に何かしらのツールで型チェックを行えるようになる可能性があります。

これがPythonに導入された、型のある世界・・・になります。

なお、付与されたアノテーションは、実行時にはチェックされません。端的に言えばコメントの延長となります。
そのため強制力はありませんが、実行時に何もしないためパフォーマンスに影響を与えることもありません。

よって原則的には静的解析のための構文になりますが、typing.get_type_hintsでアノテーション情報が取得できるため、自前で実行時チェックを実装することも可能と思います(typeannotationsなど)。

アノテーションの記法

アノテーションの記法について、typingに沿い紹介していきたいと思います。

Type aliases

型の別名を定義することが可能です。

Type aliases

Vector = List[float]

ただ、個人的な経験からするとこれは混乱を招くこともあるので(クラスなのかエイリアスなのか判別がつかなくなる)、使いどころと名前の付け方については注意が必要と思います。

Callable

関数の引数/返り値型の定義をまとめたものです。コールバック関数のように関数が引数となる場合、また関数を返すような場合に利用します。

Callable([引数型], 返り値型)という記法になります。以下は、関数を引数に取るfeeder/async_queryについて、Callableでアノテーションを行っている例となります。

Callable

from typing import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

Generics

いわゆるGenericsも利用することができます。Java等におけるList<T>を以下のように書くことができます。

Generics

from typing import Sequence, TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

また、TypeVarではGenericsとして有効な型を限定することもできます。以下では、AnyStrとしてstrbytesのみ許容しています。

pep-0484/#generics

from typing import TypeVar

AnyStr = TypeVar('AnyStr', str, bytes)

def concat(x: AnyStr, y: AnyStr) -> AnyStr:
    return x + y

User-defined generic types

いわゆるGenerics classで、MyClass<T>のようなことをしたい場合、以下のように定義します。

User-defined generic types

from typing import TypeVar, Generic

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('{}: {}'.format(self.name message))

これで、以下のように使えます。

from typing import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

The Any type

全てのtypeAnyのサブタイプになります。Anyはクラスの方のobjectとは異なるので、注意が必要です。

Union types

幾つかの許容する型をまとめたものになります。Optionalは、このUnionの一種になります。

Union types

from typing import Union

def handle_employee(e: Union[Employee, None]) -> None: ...

これは、以下と同義です。

from typing import Optional

def handle_employee(e: Optional[Employee]) -> None: ...

チェック方法

上記の通り、型アノテーションの記法は型を付けたいシーンの大体を網羅していると思います。
しかし、このままではコメントと同じで、せっかくつけたアノテーションを無視されたとしても気づくすべはありません。

Python標準では型アノテーションをチェックするための公式ツールは提供していません(3.5現在)。そのため、チェックには外部ツールを使う必要があります。

このツールの一種がmypyです。

mypy

Python本体のtypehintingのAuthorでもあるJukkaさんが開発されており、型アノテーションにおけるデファクトスタンダードライブラリといっても差し支えないと思います。

統合開発環境におけるチェックでは、PyCharmは一部?サポートをしています。以下は、strを引数に取るgreetingに数値を渡している箇所が警告になっています。

image

PyCharmは元々docstringやコメントでのアノテーションをサポートしており、今後標準に乗っ取ったサポートが追加されるのも期待できると思います。

Visual StudioのPython開発環境(PTVS)は、もともと強力な型推定があるから大丈夫という言ですが、記法の取り込みについては議論されているようです。

Use type hints in Python 3.x to infer variable types if possible #82

mypyのインストール

ここでは、mypyを使ったチェックを行うため、その導入手順について紹介していきます。

mypyのインストールはpipから可能です。

pip install mypy

pip install mypyだと全然別のライブラリが入るので注意が必要
mypy 0.470より、ライブラリ名はmypy-langからmypyに変更されました

当然Python3前提ですが(3.5でなくてもok)、Python2での対応も行われる予定のようです。
なお、2015/11/2現在では、Python3.5でmypyを利用する場合はmasterからインストールする必要があります。これは、Python3.5で削除されたUndefinedの対応がまだpypiの方には反映されていないためです(issue 639)。

GitHubのmasterからpip installするには、以下のように行ってください。

pip install git+https://github.com/JukkaL/mypy.git@master

mypyの利用

公式のQuick Startの通りとなりますが、インストール後使えるようになるmypyコマンドでチェックを実行可能です。

mypy PROGRAM

うまくいかない場合は、Troubleshootingを参考にしてください。基本はここに網羅されています。

実際に実行してみると、実行結果は以下のようになります。

>>> mypy examples/basic.py
examples\basic.py:5: error: Argument 1 to "greeting" has incompatible type "int"; expected "str"

-mオプションで、モジュールを指定することもできます。

>>> mypy -m examples
examples\__init__.py:1: note: In module imported here:
examples\basic.py:5: error: Argument 1 to "greeting" has incompatible type "int"; expected "str"

ここで、examples/__init__.pyは以下のようになっています。

import examples.basic

チェックで「In module imported here:」とあるように、パッケージを対象とする場合__init__.pyからたどれないものはチェックされないようです。そのため、モジュール内の全ファイルをチェックしたい場合は注意が必要です。

なお、既存のライブラリなどについてはその中身のソースコードにアノテーションを付けて回るわけにはいかないので、型定義を外部に切り出して定義することも可能です。この型定義のみ記述したファイルはスタブファイルと呼ばれ、その拡張子はpyiになります(TypeScriptのd.tsのイメージ)。

mypyで準備しておいたpyiファイルを読み込ませる場合は、MYPYPATHを設定します。

export MYPYPATH=~/work/myproject/stubs

なお、--use-python-pathオプションを使えばPYTHONPATH内のものは参照してくれます。

現在のところ、TypeScriptにおけるtsdのような型定義管理ツールのようなものはありませんが、python/typeshedへの集約が行われているようです(まだ数はないですが)。

この他、詳細については公式ドキュメントをご参考ください。

導入方針

TypeHintsの導入効果としては、以下のような点が期待できると思います。

  • 浅い単体テスト: 関数の誤った利用による実行時エラーを防止するためのテストとして機能させる
    • データベース周りなど、型に厳密性が要求される箇所への適用
    • 日付など、仕様がややこしいもの(UTCなのかどうか、表記方法はなどetc)について、型を明示しチェックしておく
    • 動的呼び出しなどを活用している場合の型明示
  • 仕様の明確化: 引数として何が許容され、どういった値が返ってくる可能性があるのか明示する
    • Optionalを使用することによる、None対応の必要性についての示唆
    • 呼び出す側が事前処理しておくのか、処理側で対応するのかあいまいなケースにおいて(ex.呼び出す前にキャストしておくのかどうか、要素が一つの場合は1要素の配列にしておくかなど)、仕様を明確化することで分担を明示
    • Callback的に利用する関数について、あらかじめその仕様を明確化する(デリゲート)

そして、適用対象については大まかには以下のようなものがあると思います。

  • 完全対象: 全処理が対象
  • 部分対象: DB周りの処理、特定モジュールなど、一定の基準に基づいて設定された箇所を対象とする

定められた対象に対するアノテーションの導入方法については、以下のパターンが考えられます。

  • 完全導入: 適用対象すべてについてアノテーションを付与
  • 逐次導入: 事前の設計やコードレビューなどで、つけた方がいいかどうかを逐次判断する
  • 自由導入: コメント同様、開発者が必要と感じた際につける

導入に当たっては、まずどんなメリットを期待するのかを明確にする必要があると思います。これがはっきりしていないと、適用の対象と導入方法の基準があいまいになってしまうためです。

この辺りはそもそも期待している効果が本当に得られるのかも含めて試行錯誤中なので、またノウハウが溜まってきたら追記したいと思います。

参考資料