Edited at

Python defaultdict の使い方

More than 1 year has passed since last update.


概要

Python には dict という、所謂辞書を扱う型が存在します。

dict は「key: value」の組を保持する型です。

dict でも問題ない場合も多くまたdictの方が要件に適合している場合もありますが、defaultdict を利用すると処理が簡単になる場合もあります。


dict の おさらい

Python を利用して dict を利用しない事はありませんが、dict の動作を簡単におさらいしてみます。

とりあえず、処理するサンプルデータを最初に生成しておきます。

# 英小文字で構成された、ランダムな100文字を生成

import random
import string
n = 100
val_str = ''.join([random.choice(string.ascii_lowercase) for i in range(n)])
print(val_str)

mygkgoqqllqiqeewywrtuljoujehmzebhojbzvltzmhgyqddjpdkkhudysygdeydsqpnbnomfgwwtiwlqneadrbgqmwrpcqptxli

アルファベットの出現数を数えてみます。

d = {}

print(type(d)) # type の利用で型が確認できます

for key in val_str:
d[key] += 1


<class 'dict'>
-------------------------------------------------------

KeyError Traceback (most recent call last)

2 print(type(d))
3 for key in val_str:
----> 4 d[key] += 1

KeyError: 'm'

特に何も考えずに入れようとすると、「KeyError」が発生します。

dict は「存在しないキーはエラー」になります。

よって、この場合、存在しないか確認して、キーを生成する必要があります。

d = {}

for key in val_str:
if key not in d:
# キーが存在しない場合の処理
d[key] = 0
d[key] += 1


defaultdict の 利用

defaultdict はその名前から連想できるかもしれませんが、dict の初期化を関数にしたがって実施する事ができます。そのため存在チェックが不要です。

from collections import defaultdict

d = defaultdict(int)

for key in val_str:
d[key] += 1

print(d)

defaultdict(<class 'int'>, {略})

defaultdict の引数には、「初期化時に実行する関数」を記述します。

「int」と記述した場合「lambda: int()」と同じ意味で、「int()」 は 「0」を返しますので、これは「lambda: 0」と同じ動作になります。

つまり「0を返す関数」になりますので、初期値が 0 になるわけです。

lambda は Python の無名関数です。

d = defaultdict(lambda: 0)

for key in val_str:
d[key] += 1

print(d)

defaultdict(<function <lambda> at 0x1>, {略)

ここには int() 以外にも、float()、list()、dict() 等の関数を渡す事ができます。

「list」の場合は「lambda: list()」と同じ意味になります。

d = defaultdict(list)

for key in val_str:
d[key].append(key)

print(d)

defaultdict(<class 'list'>, {略})


defaultdict の 複雑な初期化

defaultdict の引数は「関数」です。関数を書けば、いかなる初期化でも可能です。

以下のように記述すると、初期化時に初期値を「1」にする事ができます。

d = defaultdict(lambda: 1)

当然もっと複雑な事も可能です。

d = defaultdict(lambda: defaultdict(int))

d['key']['a'] += 3

print(d)

defaultdict(<function <lambda> at 0x1>, {'key': defaultdict(<class 'int'>, {'a': 3})})

d = defaultdict(lambda: defaultdict(list))

d['key']['a'].append(10)

print(d)

defaultdict(<function <lambda> at 0x1>, {'key': defaultdict(<class 'list'>, {'a': [10]})})

さらに複雑な処理が必要な場合は lambda で書くのがあまり適切ではなくなるので、関数を定義するのが良いでしょう。

def init_dict():

# 本来は複雑な関数ですが、サンプルなので、とりあえず10を返しておきます
return 10

d = defaultdict(init_dict)

d['a'] += 1

print(d)

defaultdict(<function init_dict at 0x1>, {'a': 11})


defaultdict の get 時動作は dict と同じ

get に関しては、dict と defaultdict の動作は同じになります。

d = {}

print(d.get('a'))
# Noneが返る

d = defaultdict(int)
print(d.get('a'))
# Noneが返る


defaultdict の シリアライズ

巨大データの処理では、dict をシリアライズする必要が結構あります。

シリアライズは様々な方法がありますが Python のデフォルト機能であれば「pickle」モジュールが利用されます。

# dict のシリアライズ

d = {'a': 10, 'b': 20}

import pickle
with open('dict.pickle', 'wb') as f:
pickle.dump(d, f)

# defaultdict のシリアライズ

d = defaultdict(int)

import pickle
with open('defaultdict.pickle', 'wb') as f:
pickle.dump(d, f)

ただ、lambda を利用している defaultdict の場合、そのままではシリアライズできません。

d = defaultdict(lambda: defaultdict(int))

d['key']['a'] += 3

import pickle
with open('defaultdict.pickle', 'wb') as f:
pickle.dump(d, f)

-----------------------------------------------------

PicklingError Traceback (most recent call last)

4 import pickle
5 with open('defaultdict.pickle', 'wb') as f:
----> 6 pickle.dump(d, f)

PicklingError: Can't pickle <function <lambda> at 0x1>: attribute lookup <lambda> on __main__ failed

これは Python の開発で Issue( https://bugs.python.org/issue19272 )にて改善要望が出ていますが、修正に関しては却下されているようです。

lambda ではなく、関数にしておけば、一応問題ありません。

def init_dict():

# 本来は複雑な関数ですが、サンプルなので、とりあえず10を返しておきます
return 10

d = defaultdict(init_dict)

d['a'] += 1

import pickle
with open('defaultdict.pickle', 'wb') as f:
pickle.dump(d, f)

ただ、一々対応するのが面倒な場合は外部モジュールの「dill ( https://pypi.org/project/dill/ )」のような pickle のラッパーや、「MessagePack ( https://pypi.org/project/msgpack/ ) 」等「pickle」以外のシリアライズライブラリを利用する方が良いでしょう。

pip install msgpack

d = defaultdict(lambda: defaultdict(int))

d['key']['a'] += 3

import msgpack
with open('defaultdict.msgpack', 'wb') as f:
msgpack.pack(d, f)


defaultdict を dict に変換する

defaultdict は既存に存在しない key が入ってもデフォルト値を生成するのが利点です。

それは同時に欠点でもあり、意図しない key であっても値を生成する、という事を意味します。

存在しない key が追加されたらエラーになってほしい場合は利用できないのは当然の事となります。

defaultdictで一旦値の生成が完了したらdict に変換し、それ以降は不正keyはエラーにしたい、という場合は defaultdict を dict に変換する必要があるかもしれません。

dict の引数に渡せば、型を変換できます。

d = defaultdict(lambda: defaultdict(int))

d['key']['a'] += 3

print(type(d))

d = dict(d)
print(type(d))


書いた人に関して

Tech Fun株式会社スペシャリスト、xza です。

社内で開催した初学者向け勉強会で利用した資料等を整理した上で公開しています。