def main(args: Any) -> None:
Python の魅力は何と言っても型を明示せずにかけることですよね。そのおかげで数分で書いた小さなスクリプトでも大抵のことができます。しかし動的すぎるがゆえに、規模が大きくなるにつれ、これが裏目に出ることもあります。人間の脳が一度に保持できる情報量はたかが知れていますから、やっぱりどこかで型の不一致からくるバグが起こりえます。これに嫌気が差したプロフェッショナルなプログラマーたちは考えました。じゃあ、 Python も type-aware にすればいんじゃね、と。
その甲斐あって Python3.5からは関数の引数や戻り値に型を明示できるようになりました。あくまでも選択肢の一つとして、やりたければやっていいという感じに使用者の裁量に任されていますが。
def fizzbuzz(num: int) -> None:
[print("fizzbuzz") if not x % 15
else print("fizz") if not x % 5
else print("buzz") if not x % 3
else print(x) for x in range(num)]
今年(2016年)の末頃までには Python3.6 が出ます。そこではこれを更に発展させて、変数にも型を明示できるようになりますよね。
from typing import List, Dict primes: List[int] = [] captain: str # Note: no initial value! class Starship: stats: Dict[str, int] = {}
しかし当然ながら、コードの本体中に型を書いてしまうと、このコードは2系では動きません。3系でも2系でも動かせるよう、素晴らしい代替案が用意されています。コメントに書く docstring とは違います。型の情報だけを外部に切り出すのです。
スタブが生まれた
すなわち、型の情報だけを書いたファイルを作り、それを元にして先のコードの型チェックをするということです。PEP 484で提唱されたこれをスタブファイルといいますが、その中身はチェックしたい Python コードの骨組みです。これを、型チェックの機能を持つツールに渡すと、不具合の源を指摘してくれるというわけです。mypy (mypy での作り方: Creating Stubs For Python Modules) や PyCharm で使えます。ファイルの書き方自体はどちらでも同じです。
対象は
「自作モジュールの構成は把握している、そうじゃなくてライブラリを使う時にコード補完を賢くさせたいんだ」という方は typeshed: Collection of library stubs for Python, with static types へどうぞ。有志によって、標準モジュールや外部のライブラリの為のスタブファイルが作られています。
ライブラリではないスクリプトに適用する意義は、 Python のバージョンに依らず実行できるようにすることです。また、 IDE の補完の精度が上がるため、開発がしやすくなるのがメリットだと思います。
どんな感じなの
それでは、実際に作ってみましょう。スタブファイルの拡張子は .pyi
です。それを検査したいコードと同じディレクトリーに置きます。型情報をスタブに移してしまえば、コード本体についていた方(あれば)は消しても大丈夫です。ちなみに PyCharm では新規作成の一覧に .pyi
はありません。ありませんが、手動で作成すると自動で認識して、以降こちらを参照するようになります。型推測の優先順位は、 docstring < コード中の直書き < スタブ のようです。
Before
こんなコードがあったとします。説明用にできるだけ要素を詰め込んで作ったので不自然なコードかもしれませんが、一応動くものです。
まずは型情報が一切ないどこにでもある普通のコード。
import json
import logging
import requests
import sys
class Resources:
POST_URL = "https://httpbin.org/post"
def __init__(self, auth, logger):
self.auth = auth
self.logger = logger
self.session = self.get_session()
self.res = self.get_resources()
def get_session(self):
return requests.session()
def get_resources(self):
return json.loads(self.session.post(
self.POST_URL, params=self.auth).text)
def get_infos(self, queue):
if isinstance(queue, str):
return str(self.res.get(queue, ""))
else:
return {key: self.res.get(key, "") for key in queue}
class FooLogger(logging.Logger):
def __init__(self):
super(FooLogger, self).__init__("foobar", logging.INFO)
self.logger = logging.getLogger()
log_stdout = logging.StreamHandler(sys.stdout)
self.addHandler(log_stdout)
r = Resources({"name": "watashi", u"文字列": u"もじもじ"}, FooLogger())
print(r.get_infos(["args", "origin"]))
print(r.get_infos("origin"))
これに型情報をつけるとこのようになります。この時点で3系専用になりました。
from typing import List, TypeVar, Union, Dict, Text
import json
import logging
import requests
import sys
DatabaseType = Dict[Text, Union[int, Text, Dict[Text, Text], None]]
LoggerType = TypeVar("LoggerType", bound=logging.Logger)
class Resources:
POST_URL = "https://httpbin.org/post"
def __init__(self, auth: Dict[Text, Text], logger: LoggerType) -> None:
self.auth = auth
self.logger = logger
self.session = self.get_session()
self.res = self.get_resources()
def get_session(self) -> requests.Session:
return requests.session()
def get_resources(self) -> Dict:
return json.loads(self.session.post(
self.POST_URL, params=self.auth).text)
def get_infos(self, queue: Union[List[Text], Text]) ->\
Union[DatabaseType, Text]:
if isinstance(queue, Text):
return str(self.res.get(queue, ""))
else:
return {key: self.res.get(key, "") for key in queue}
class FooLogger(logging.Logger):
def __init__(self) -> None:
super().__init__("foobar", logging.INFO)
self.logger = logging.getLogger()
log_stdout = logging.StreamHandler(sys.stdout)
self.addHandler(log_stdout)
r = Resources({"name": "watashi", "文字列": "もじもじ"}, FooLogger())
print(r.get_infos(["args", "origin"]))
print(r.get_infos("origin"))
After
これに対するスタブファイルの中身はこうなります。# type:
と書いてあるのは変数の型を明示する為の記法です。
from typing import List, TypeVar, Union, Dict, Text, overload
import logging
import requests
# エイリアス
DatabaseType = Dict[Text , Union[int, Text , Dict[Text , Text], None]]
# ジェネリクス
LoggerType = TypeVar("LoggerType", bound=logging.Logger)
class Resources:
POST_URL = ... # type: Text
def __init__(self, auth: Dict[Text , Text], logger: LoggerType) -> None:
self.auth = ... # type: Dict[Text , Text]
self.logger = ... # type: LoggerType
self.session = ... # type: requests.Session
self.res = ... # type: Dict
def get_session(self) -> requests.Session: ...
def get_resources(self) -> Dict: ...
@overload
def get_infos(self, queue: Text) -> Text: ...
@overload
def get_infos(self, queue: List[Text]) -> DatabaseType: ...
class FooLogger(logging.Logger):
def __init__(self) -> None:
super().__init__(self, ...)
self.logger = ... # type: LoggerType
説明
まずスタブはコードの概観を表すものなので、実装は書かなくて良いんです。処理の本体は「...」で省略します。また、初期値が設定されている引数や定数もいくつかありますが、全て「...」です。ゆえにこれは Python の形をした別物であって、実行は出来ません。
@overload
スタブ特有の要素は実は一つだけなんです。ほかは typing モジュール自体の使い方と同じです。その一つがこれです。
上のコードで get_infos() は、リストが与えられると辞書を返し、文字列を与えられると文字列を返します。本体のように、
def get_infos(self, queue: Union[List[str], str]) -> Union[DatabaseType, str]:
と書いてしまうと、 リスト→リスト なのか、 リスト→文字列 なのか区別できません。こういうときにオーバーロードの出番です。引数と戻り値の型の組み合わせを明確にできます。
文字列
2系の対応も考えるなら、文字列の型が str
では不自然です。 Text
は「文字列」として、3系では str
で、2系では unicode
として振る舞ってくれます。 bytes
も含めたい場合には AnyStr
が用意されています。
数字
int
と float
両方を表すジェネリクスは用意されていないようです。なので
T = TypeVar('T', int, float)
とするのが無難でしょうか。
List
, Tuple
, Dict
など
通常のlist
、tuple
、dict
に対応するものです。 typing モジュールからインポートしていますが、せずに小文字の方を使っても問題ありません。というのも実装を見ると
class List(list, MutableSequence[T], extra=list):
def __new__(cls, *args, **kwds):
if _geqv(cls, List):
raise TypeError("Type List cannot be instantiated; "
"use list() instead")
return list.__new__(cls, *args, **kwds)
となっているため、ただのlist()
と大差ないようです。ちなみに Dict[str, str]
は 「キーの型が str 、値の型が str 」ということです。Dict[str]
は無効です。
Union
と Optional
Union は文字通り、何かと何かが合わさっているものを表現するときに使います。
Union[int, str]
では int と str のどちらも受け取れる(or 返す)ことを表します。
Optional は、 Union の要素の一つが None
であるものを表します。つまり、
Optional[str] == Union[str, None]
です。昨今話題の null 安全を実現する(かもしれない)ものです。
エイリアス
get_infos() は辞書を返します。もちろん単に Dict と書くのでも十分ですが詳細に定義するとどうなるでしょう。
Dict[str, Dict[str, Union[int, str, List[str]]]]
こんなに長いものを何回も書くのは面倒です。コピペはバグのもとです。変数に丸め込んでしまいましょう。これをエイリアスと言います。
ジェネリクス
FooLogger はログ出力用のクラスですが、メインの処理にとってはその具体的な名前はどうでもよく、ただ logging を継承しているかどうかが重要です。そういう時にこれを使います。 Python 流のジェネリクスの書き方はこうです: T = TypeVar("T", bound=logging.Logger)
この場合 T
は logging のサブクラスです。一般にはbound
を指定せずに T = TypeVar("T")
と書くことの方が多いと思います。
ちなみに LoggerType = TypeVar("T")
は駄目です。変数名と文字列の中の型名は一致していないといけません。