マイクロアドアドベントカレンダー三日目担当の記事です。
Pythonは単純でわかりやすいプログラミング言語のため、欲しいデータ構造を自ら実装してしまうことが良くありますが、標準ライブラリに目を通すと多くのデータ構造はすでに用意されている事が多いです。
今回は自戒を込めてPython3のcollectionsライブラリについて紹介します。
個人的に使えると感じた順番で紹介します。
- Counter
- defaultdict
- OrderedDict
- ChainMap
- deque
- namedtuple
Counter
リストやイテレータからすべての値の出現回数をカウントします。
>>> from collections import Counter
>>> Counter([1,1,2,3,4,4,4,5]) # listはシーケンス型
Counter({4: 3, 1: 2, 2: 1, 3: 1, 5: 1})
>>> Counter("Qiita") # strはシーケンス型
Counter({'i': 2, 'Q': 1, 't': 1, 'a': 1})
データのたくさん入ったCSVなどをPythonを利用して集計することがあるのですが
今までデータの出現回数を出したい時には辞書型とfor文を組み合わせてカウントしていました。
Counterを導入するとコードが短く、見やすくなったため便利なデータ構造です。
ちなみに一種類の値だけの集計であればcountというリストのメソッドも使うことができます。
defaultdict
存在しないキーを参照した際にデフォルト値が入った状態になっている辞書型風のデータ構造です。
>>> from collections import defaultdict
>>> d = defaultdict(int)
>>> d[0]
0
>>> d = defaultdict(lambda:100) # 100が返ってくるラムダ式
>>> d[1]
100
ちなみに標準の辞書型で同じことをすると例外が発生します。
自分で書き換えられる箇所ならgetを使用することで回避することもできるのですがそうは行かない場合が多いと思います。
>>> d = dict()
>>> d[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 0
一つのプログラムで複数の状態を管理したいときには辞書型を良く使用するのですが、
今まで新しいキーが出現した際にはin
や例外をキャッチしてデフォルト値を代入していました。
長いだけならまだしもうっかりバグを発生させていたりしたのですが、
defaultdictを使用することでシンプルかつバグりにくいコードが書けるようになりました。
OrderedDict
値を追加した順序を記憶する事ができる辞書型風のデータ構造です。
>>> from collections import OrderedDict
>>> o=OrderedDict()
>>> o['first']=1
>>> o['second']=2
>>> o
OrderedDict([('first', 1), ('second', 2)])
とても便利そうで使用したいと良く思うものの、順序の情報は別のオブジェクトや辞書型のvalueに持たせることが多いため使用したことがありませんでした。
このデータ構造を用いるとfor文内で順序付けのためのカウントなどを行う必要がなくなるため綺麗なプログラムが書けると思います。今後使用していきたいです。
ちなみにPython3.7以降はdict自体が順序を保証ようになったので、多くの場合でOrderedDictは必要なくなりました。
ChainMap
辞書をそのまま連結できるデータ構造です。
>>> from collections import ChainMap
>>> class DictLike(dict): # dictを継承したクラス
... def __missing__(self, key):
... return key
... def __repr__(self):
... return 'DictLike()'
...
>>> dict_like = DictLike()
>>> dict_like['unknown'] # キーがそのまま返ってくる
'unknown'
>>> # 通常の結合
>>> c=ChainMap({1:2,3:4}, {5:6,7:8})
>>> c
ChainMap({1: 2, 3: 4}, {5: 6, 7: 8})
>>> # dictライクな辞書を結合
>>> c=c.new_child(dict_like)
>>> c
ChainMap(DictLike(), {1: 2, 3: 4}, {5: 6, 7: 8})
>>> c[3]
4
>>> c['unknown']
'unknown'
結合の際にdictに強制的に変換せずに内部にそのまま保持する性質があるので、dictを継承したdict-likeオブジェクトと普通のdictを一緒に取り扱いたい際に便利です。
もちろん、DefaultDictやOrderedDictの結合にも使用できます。
また、通常のdictに比べてdict同士の結合の速度が早いです。
deque
双方向から値の挿入、取り出しを行えうことができるスレッドセーフなキューです。
>>> from collections import deque
>>> d=deque([1,2,3,4])
>>> d.pop()
4
>>> d.popleft()
1
>>> d
deque([2, 3])
>>> d.appendleft(4)
>>> d.append(1)
>>> d
deque([4, 2, 3, 1])
別のスレッドに仕事を渡したいときはスレッド安全なQueueという標準ライブラリを使用する事が多いと思うのですが、
キューに割り込みの概念が欲しい時があり、そういう時に役に立ちました。
他の特徴として、Pythonでスタックが必要な時はlist型を使用することが多いと思いますが、dequeをスタックとして使用したほうがパフォーマンスが良い傾向があります。
ちなみに、他のスレッドにもっとしっかり優先度管理しつつタスクを渡したい場合には標準ライブラリのheapqという物を使用することができます。
namedtuple
tuple型の要素にクラスのメンバ風にアクセスできるようにするデータ構造です。
>>> from collections import namedtuple
>>> N=namedtuple('Fruit',['apple','grape'])
>>> N(*[1,2]) # リストを展開
Fruit(apple=1, grape=2)
>>> n=N(*[1,2])
>>> n
Fruit(apple=1, grape=2)
>>> n.apple
1
>>> n.grape
2
使用するデータの構造が固定かつ値の読み出しが多い状況でコードの見やすさが向上するものの、
コードが短くなるわけでもないため大規模なプロジェクトでない限りあまり使用しないです。
使用する際にはPython3.7で追加されたdataclassesも検討したほうが良いです。