はじめに
クソデカ天使様、なんて神々しい...、nikkieです😇
読み始めました、『ロバストPython』!1
将来の開発者に意図を伝えるPythonの書き方が指南された(議論のための)本です。
直近でコレクションについて少し調べていたこともあり2、「5章 コレクション型」に食指が動きました。
目次
- はじめに
- 目次
- 5章 コレクション型
- 5章 コレクション型を読んでの3つの学び
- 1. 型ヒントの書きづらさには異種コレクションが関係していた!
- 2. collectionsのUserDictたちの使い所がようやく分かった!
- 3. ジェネリクス、完全に理解した
- 終わりに
- P.S. 思い出した記事たち
5章 コレクション型
公開されている目次から引用します。
コレクション型とは、データの集まり、すなわち任意の数のデータを格納する型で、例を挙げると、リストや辞書などです。
これらへの型ヒントの書き方や、独自のコレクション型の作成が指南されます。
5章 コレクション型を読んでの3つの学び
5章の内容は学びが多くありました。
- 異種コレクションという概念により、型ヒントが書きづらいなと感じていた事象が言語化された!(5.2, 5.3)
collections
のUserDict
たちの使い所がようやく分かった!(5.4.2)- ジェネリクス、完全に理解した(5.4.1)
小さい学び
- コレクション型は要素の型も明示する(5.1)
- 例:
list
はlist[Any]
なので、list[要素の型]
とする
- 例:
collections.abc
についての指南(5.4.3)
以下のコードはPython 3.10.9、mypy 1.1.1で型チェックしています。
1. 型ヒントの書きづらさには異種コレクションが関係していた!
異種コレクション
5.2で導入される概念が同種コレクション・異種コレクション。
コレクションの要素のデータ型が同種か異種かという定義です。
- 同種コレクションの例:リスト、辞書
- 反復処理される3
- 異種コレクションの例:タプル
- 反復処理されない
異種コレクションのタプルの型ヒントの例です。
例:料理の本。書籍名(str
)とページ数(int
)
Cookbook = tuple[str, int] the_book: Cookbook = ("レシピボン", 45)
タプルは添字(インデックス)アクセスとなりますが(the_book[0]
)、「フィールドの名前でアクセスしたい!」と辞書が導入されます。
異種コレクションの辞書に起因する、型ヒントの書きづらさ
同種コレクションとして想定されている辞書を異種コレクションとして使うから型ヒントが書きづらいのか!という気付きがありました。
タプルの例を辞書で書き換えていきます。
from typing import Union Cookbook = dict[str, Union[str, int]] the_book: Cookbook = {"name": "レシピボン", "page_count": 45}
料理の本を表す辞書の型ヒントは以下のようになりました
- キーは常に
str
("name"
と"page_count"
) - 値は
str
(書籍名)またはint
(ページ数)- 👉
Union
を使って表す
- 👉
この辞書にフィールドが追加され、それがfloat
やはたまた別のコレクションだったらどうでしょうか?
辞書の値の型ヒント(Unionを使った型ヒント)は(型エイリアスを使ったとしても)どんどん煩雑になっていきます。
これを見て、辞書を異種コレクションに使うと型ヒントが書きづらいと言語化ができました。
これらの問題は、すべて同種コレクションに異種データを格納するから生じたものだ。
この場合、コレクションの利用者に負担を押しつけるか、型アノテーションを全く放棄するかになる。(5.2)
異種コレクションとして辞書を使う場合の型ヒントにはTypedDict
を使う
Python 3.8で導入されたTypedDictは、辞書にどうしても異種データを格納しなければならない場合のために作られたものだ。(5.3)
https://docs.python.org/ja/3/library/typing.html#typing.TypedDict
料理の本に適用してみます。
from typing import TypedDict class Cookbook(TypedDict): name: str page_count: int cookbook: Cookbook = {"name": "レシピボン", "page_count": 45}
TypedDict
を使うとUnion
が不要になってかなり分かりやすくなった感覚があります! すごい
TypedDict
はあくまで型チェッカのためのもので、実行時にバリデーションされるわけではないそうです。
辞書を異種コレクションで使うケースですが、APIから返ってくるJSONや、YAMLなど種々のファイル読み込みが挙がっていました。
そんなときはTypedDict
を使うことで、明確な型ヒントが書けそうです。
サンプルコードの中に、参考にしたい例(APIの返り値が複雑な例)がありました。
TypedDict
を教えてくれてありがとう、『ロバストPython』
2. collections
のUserDict
たちの使い所がようやく分かった!
標準ライブラリcollectionsにUserDict・UserList・UserStringがあります。
「これらの使い所がはっきりしないな〜」とずっと感じていたのですが、5.4.2を読んで分かりました。
既存のコレクション型の一部のメソッドの動作を変えるときです。
__getitem__
メソッドの動きを変えた辞書を作る例が紹介されました。
書籍には、なぜ組み込みのdict
を継承して__getitem__
をオーバーライド5してもうまくいかないかも解説されています。
dict
の継承でうまくいかないからUserDict
の出番なのですね!
3. ジェネリクス、完全に理解した
型ヒントのジェネリクスって、なんだかよくわからない不気味さがあったのですが、『ロバストPython』5.4.1の解説で完全に理解できました!6
ジェネリック型は、どのデータ型を使っても構わないことを示すためのもの (5.4.1)
紹介されたもの
typing.TypeVar
:型変数typing.Generic
:ジェネリック型のための抽象基底クラス
typing.TypeVar
TypeVar
の例は、リスト(要素はなんでもよい)を逆順にして返すような関数の型ヒントです。
引数と返り値でリストの要素の型は共通です。
これがTypeVar
で表せます
from typing import TypeVar T = TypeVar("T") def reverse(coll: list[T]) -> list[T]: # type: ignore[empty-body] # collを逆順にして返す処理 ... numbers: list[int] = reverse([1, 2, 3]) characters: list[str] = reverse(list("デパプリ"))
またコラムでは、ジェネリクスを使ってAPIのエラー処理も単純化できる例も示されました7。
typing.Generic
グラフの例で解説されます。
class Graph(Generic[Node, Edge]): def __init__(self): self.edges: dict[Node, list[Edge]] = defaultdict(list) def add_relation(self, node: Node, to: Edge): self.edges[node].append(to) def get_relations(self, node: Node) -> list[Edge]: return self.edges[node]
edges
属性はキーをNode
型、値をlist[Edge]
型とする辞書(同種コレクション)add_relation
メソッドは、Node
型の引数node
とEdge
型の引数to
を受け取るget_relations
メソッドはNode
型の引数node
を受け取り、list[Edge]
型の値を返す
ジェネリクスを使うことで
あらゆる要素型のグラフを定義でき、かつそれらの型チェックを成功させられる。(5.4.1)
サンプルコードでは3つのグラフを定義していました。
Graph[Restaurant, Restaurant]
Graph[Recipe, Recipe]
Graph[Restaurant, Recipe]
言ってみれば、型ヒントで使う型を変数のようにしておくことで、要素の型を変えても定義できるわけですね。
なるほど、これがジェネリクス!
サンプルコードで使っているRestaurant
やRecipe
はtyping.NewType
で定義していました。
https://docs.python.org/ja/3/library/typing.html#newtype
Restaurant = NewType('Restaurant', str)
という実装なので、型チェックでは第2引数str
のサブクラスとして扱われるみたいです。
終わりに
『ロバストPython』5章からの自分にとっての学びをアウトプットしました。
この中ではTypedDict
を知られたのが大きいですね。
型ヒントが書けないな〜と思っていたコードにTypedDict
で立ち向かえそうです。
ジェネリクスもいままで避けてましたが、今回知ったのでもうちょっと素振りするなどして練度を上げたいな〜と思います。
非常に学びの深い章でした。
コレクションに関する型ヒントの難しさがスルッと解消した感がありますので、同じようなお悩みを抱えている方は読んでみると発見が多いのではないかと思います。
そして、やっぱり読書会があるとよさそうに思います〜(どなたかご一緒しませんか〜?📣)
型チェック自分用メモ
- 型を変数に代入すると型エイリアス
TypeVar
で型変数を代入できるNewType
でサブクラスの型を作れる
P.S. 思い出した記事たち
5章を読んでいて思い出した記事を貼っておきます。
異種コレクションの辞書にUnionと型エイリアスで型ヒントする例はPython Monthly Topicsで知っていました。
改めて見ると、この記事ではTypedDict
も紹介されていたのですね!
もう一つ、メルカリさんのこちらの記事は、型エイリアスやTypedDictの代わりに、データ構造を定義して型ヒントするアプローチです(typing.NamedTuple
や@dataclasses.dataclass
)。
- 『ロバストPython』読み始めました 📘「あなたの書くPython、将来の開発者に意図、伝えていますか?」 私(Python歴5年)「すいませんでしたあああ!🙇♂️ やり方マジで教えてください」 - nikkie-ftnextの日記↩
- ミノ駆動本_読書py 7章コレクションの予習です。一例:Pythonのイテラブルとイテレータ 〜for文の秘密〜 - nikkie-ftnextの日記↩
- ファーストクラスコレクションのPythonでの実装例を見て考えたこと - nikkie-ftnextの日記 では デザインと歴史 FAQ を参照しています↩
- https://docs.python.org/ja/3/library/typing.html#type-aliases↩
- https://github.com/pviafore/RobustPython/blob/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter5/overriding_dict.py↩
- この勢いで使っていく中で、近い未来になにもわからなくなるでしょう(ダニング・クルーガー効果)↩
- ↩