イントロダクション
最近オライリーの『プログラミングTypeScript』を読み、非常に感銘を受けました。
ジェネリクスの奥深さ面白さ、型がある事による関数やオブジェクト情報の増加、そして生まれる安心感……。型無しでは不安で昼寝できない身体になってきました。
そんな折『飛騨高山Pythonモクモク会』で今月も発表することになり、大概ネタ切れ状態だった私は思いました。そうだ、めっちゃ感銘を受けたTypeScriptを絡めれば楽しく発表できるぞ、と。
幸いPythonにも型ヒントがあります。Pythonの型システムはTypeScriptに比べれは少々貧弱だと思っていたのですが、調べてみたらなかなかどうして! これは便利という機能が揃っていました(特に3.8以降)
この記事ではシリーズを通して型システムの奥深さ面白さ便利さを少しでもお伝えできればと思っています。
##完全にネタかぶりしました
途中まで書いた時、ふとPyConJP2020の演目を見ていたら『Python 3.9 時代の型安全な Pythonの極め方 by.小笠原みつき』というのがあるではありませんか。しまったなぁと思ったのですが、まぁ書いちゃったのでそのまま行くことにしました。
皆が皆同じ事書くわけじゃないしね。
3.9以降の記法など一部参考にして、今回の内容に反映しています。
##対象読者
- JavaScript/Python等、型の無い言語の方々
これらの方々の中には、型と言われるとこういうのをイメージする人がいるかもしれません。
型の存在は筋力(性能)を上げるけど、その分堅苦しくって動きを制約されたプログラミングを強いられる。
しかし、実際はそうではありません。型はプログラマブルで、しかも生産性を上げるものです。
最後まで読んで頂ければ、それがきっと理解できるはず。いや、理解できる。できるんじゃないかな。……理解して頂けるよう、頑張って書きます。
- CとかJavaとかなんでもいいけど古典的型を使った事がある方々
これらの方々にも新しい発見があると思います。
動的言語に後付で型を付けると意外と面白い事になった。それがTypeScriptや型ヒントです。ガチガチに型のある言語経験者にも、Union型やLiteral型の興味深い仕組みには驚きと発見があるのではないでしょうか?
##Pythonタイプヒントの特性
ここではPythonタイプヒントの特性とその特性から見えてくる、用途について述べます。
###型情報は実行時には完全に無視される
型情報は実行時には無視されます。ですので
- 型を書いたところで普通にpythonコマンドで実行しても意味がない
- 実行時の型エラーは防げない
- 速度向上も見込めない
というなんだか一見微妙な代物、言ってしまえばタイプヒントはただのコメントです。……みなさんのモチベーションがダウンしてきているのが見えます。
しかし、逆にタイプヒントをコメントとして見ると、その強烈さが浮かび上がってきます。
- タイプヒントは型についてのコメントである
- タイプヒントはmypyや各種IDEとの組み合わせで、コード内に各種制約を課す事ができる
- 型の合成等の型演算ができる。つまり、コメントなのに関数が使える
いかがでしょう、そう考えるとなんだか面白くありませんか? そしてとても強力に見えるでしょう。実際、強力なのです。
さて、皆さんのモチベーションが少しは回復してきたかと思います。そろそろ実践へと入っていきましょう。
##VSCodeで事前準備
今回はVSCodeとPyrightで実験しました。VSCodeにpyrightを入れて設定するには、以下を参照してください。
https://qiita.com/simonritchie/items/33ca57cdb5cb2a12ae16
入れるとこんな感じになります。うっとおしい波線が出てきます。
極論を言うとこのうっとおしい波線こそが型ヒントを使う、究極のメリットです。
##型の書き方
変数名: 型 = 値
##基本的な型
####int:整数型
i: int = 2
i = 1.1 ##type error! # 1.1はfloatなのでエラー
型を決めた変数に型違いのものを入れると、うっとおしい感じの波線が出ます。うっとおしいので直しましょう。
####float:浮動小数点
f: float = 1.0
f = 1 # 1はint型ですが、キャスト(型変換)が入りエラーになりません
####complex:複素数型
c: complex = 1 + 2j
####str:文字列型
s: str = 'hoge'
####byte:バイト配列
byte: bytes = b'test'
####bool:真偽値
b: bool = True
##型引数を取る型
ここから紹介する型は他の型を型引数(引数のようなもの)に取るすげー型です。いわば型の抽象化とプログラミングです。
typingを利用して記載します。
import typing
記法としては以下のようになります。
変数名: 型名[型引数] = 値
####list:リスト
lst: typing.List[int] = [1, 2, 3]
lst2: list[int] = [0,1,2] # Python3.9以降で使える記法
もうちょい自由の効くリストは作れんのかと思った方、後で出てきます。
####set:重複なしのリスト
st: typing.Set[int] = {1, 2, 3}
####tuple:型、長さ指定の固定長配列
全ての型を記載します。
tpp: typing.Tuple[int, bool, str] = (1, True, 'test')
####dict:辞書
Dict[キーの型, 値の型]
dic: typing.Dict[str, int] = {'name': 1, 'age': 2}
dic1: dict[str, int] = {'test': 1} # Python3.9以降で使える記法
これじゃ複雑な辞書使えないじゃんと思った方に朗報です。後でもっときめ細かい指定のできる方法が出てきます。
##型エイリアス
型情報ってなんか長げーなと思ってきたころかと思います。
そんなせっかちな貴方や、せっかく作った型には名前を付けてわかりやすく管理したいという実利的な貴方、そんな方々の為に型の変数版とでも言うべき型エイリアスはあります。
TupleIBS = typing.Tuple[int, bool, str]
##選択できる型
####Union:列挙型
Union[型1, 型2, 型3 ...]
型引数に列挙した型のどれになっても良い型です。
uni: typing.Union[int, bool] = 1
uni = False
uni3: int | bool = 1 # Python3.10から使えるようになる記法
型は組み合わせる事でさらなる効果を発揮します。
先程、より柔軟なリストについて言及しました。それをそれをここで実現しましょう。
list_ib: typing.List[typing.Union[int, bool]] = [1, True, 2, False]
####Literal:リテラル型 (※Python3.8以降)
神機能です。~~なぜ日本語圏での言及が少ないのかわからないほど神機能です。舐めてんのか。~~PyConJP2020の公演で言及がありました。
一言で言えば、定数の型化です。より限定的な型表現ができます。おまけに列挙機能まであります。強い。
li1: typing.Literal[1] = 1 # 実質定数
li2: typing.Literal['hoge', 'fuga'] = 'fuga' # 複数記載可能
li2 = 'hoge'
####Final (※Python3.8以降)
イミュータブル(普遍)を表現できます。が、ちょっとした落とし穴あり。
c: typing.Final[str] = 'c'
c = 'a' ##type error!
lis: typing.Final[typing.List[int]] = [1,2,3]
lis[1] = 1 # エラー出ず
まぁこういうケースは素直にtuple使いましょう。
####TypedDict:型付の辞書 (※Python3.8以降)
よりきめ細かく辞書を型指定できます。
そしてこれをじっくり読み、そして実際に書いてみれば、型システムの便利さが感じられるはずです。
# クラスの記法を使って辞書の型情報を記載します
class UserDict (typing.TypedDict): # TypedDictを継承する
name: str
age: Optional[int] # None可能にする ≒ Union[int, None]
sex: typing.Literal['men', 'women', 'other']
users: typing.List[UserDict] = [
{
'name': 'noji', # 文字列のみ
'age': 33, # 数値かNoneのみ
'sex': 'men' #「漢」とかは入れられません。入力補助も効きます。
},
{
'name': 'superwomen',
'age': None,
'sex': 'women'
}
]
#まとめ:型で楽をしよう
今回は基本的な(ほぼクラスを含まない)基本的な型について解説しました。
型はプログラム可能で、IDEにも作用する有用なコメント、と言った意味が見えてきたのでは無いかと思います。
ぱっと思いつくだけでも、辞書型や引数の選択肢を狭めるという用途が考えられます。オプションを文字列で指定する場合等に、Literal型がとても役に立ちます。
形態の解っているJSONを拾って利用する時に、悩みを減らす事にもなります。
型がわかっていればインテリセンスも効くので、慣れないクラスを使うときもバッチリです。
元々型の無い言語に型を付けたので、型の付け方も柔軟に行なえる点も嬉しいです。
型を使うとちょっとの手間で大きな楽ができるのです。プログラマ三大美徳のうちの怠惰を極める為の大いなる助けとなります。
さぁ、型を使って楽で楽しいプログラミングをしましょう。
##付録:型で苦しんだ時は
でもまぁぶっちゃけ。型で苦しむときもあります。
俺はロジックを書きたいだけなのに、なんでこんな型エラーと戦ってんだ? みたいな。そんな時はまぁ、妥協しちゃってもいいさと考えましょう。
###型の推論
一度も変数を書き換えない限りにおいては、型推論が有効に働くので実はわざわざ型は書かなくても良いです。
実際のところ、僕も型情報は以下のものしか書きません。
- 関数の引数
- 関数の戻り値(推論に任せる事もある)
- 再代入がありえる変数
boo = True # IDEが自動的にbool型であると推論し、それに剃った動作をする
boo = 1 # しかし書き換えた瞬間に型が変わる(もしくはAnyになる)
###cast:型の変換
型情報が無かったり困った時は使ってみよう。なかなかどうして役立ちます。強引な変換ができるので、乱用厳禁です。
※実行時には何もしません
i3: int = typing.cast(int, 1.0)
###Any:なんでもあり型
これ即ち型ヒントの無いPythonの世界です。なかなかの劇薬です。
ani: typing.Any = 1
ani = 'hoge'
ani = ['fuga', 129999, True]