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
型の別名を定義することが可能です。
Vector = List[float]
ただ、個人的な経験からするとこれは混乱を招くこともあるので(クラスなのかエイリアスなのか判別がつかなくなる)、使いどころと名前の付け方については注意が必要と思います。
Callable
関数の引数/返り値型の定義をまとめたものです。コールバック関数のように関数が引数となる場合、また関数を返すような場合に利用します。
Callable([引数型], 返り値型)
という記法になります。以下は、関数を引数に取るfeeder/async_queryについて、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>
を以下のように書くことができます。
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としてstr
、bytes
のみ許容しています。
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>
のようなことをしたい場合、以下のように定義します。
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
全てのtype
はAny
のサブタイプになります。Any
はクラスの方のobject
とは異なるので、注意が必要です。
Union types
幾つかの許容する型をまとめたものになります。Optional
は、このUnion
の一種になります。
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
です。
Python本体のtypehinting
のAuthorでもあるJukkaさんが開発されており、型アノテーションにおけるデファクトスタンダードライブラリといっても差し支えないと思います。
統合開発環境におけるチェックでは、PyCharmは一部?サポートをしています。以下は、str
を引数に取るgreeting
に数値を渡している箇所が警告になっています。
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周りの処理、特定モジュールなど、一定の基準に基づいて設定された箇所を対象とする
定められた対象に対するアノテーションの導入方法については、以下のパターンが考えられます。
- 完全導入: 適用対象すべてについてアノテーションを付与
- 逐次導入: 事前の設計やコードレビューなどで、つけた方がいいかどうかを逐次判断する
- 自由導入: コメント同様、開発者が必要と感じた際につける
導入に当たっては、まずどんなメリットを期待するのかを明確にする必要があると思います。これがはっきりしていないと、適用の対象と導入方法の基準があいまいになってしまうためです。
この辺りはそもそも期待している効果が本当に得られるのかも含めて試行錯誤中なので、またノウハウが溜まってきたら追記したいと思います。