みんな大好きcollections
では大抵のものは用意されてて面白いです。
defaultdictを返すdefaultdictを作って、KeyErrrorの発生しない世界を作る
defaultdict
はキーが見つからなかった場合の挙動を設定できるdictの拡張クラスです。
8.3. collections — 高性能なコンテナ・データ型 — Python 2.7ja1 documentation
キーの有無での振る舞いの場合分けが不要になるので、集計処理などで利用するとたいへん便利です。
使用例の転載になりますが、これが一番わかりやすいでしょう。
>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
>>> d = defaultdict(list)
>>> for k, v in s:
... d[k].append(v)
...
>>> d.items()
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
カウンターとして使う例もありますが、単純なカウンターとしての利用であればcollections.Counter
1を検討することをお勧めします。
入れ子の辞書で利用する
例えば、入れ子の辞書でd['a']['b']['c']
という呼び出しをする場合、もしd
がdefaultdict
だったとしても、d['a']
のバリューが通常のdict
だとそこでKeyErrror
が起きるかもしれません。
そこで、defaultdict
に渡すdefault_factory
を以下のようにしてみます。
def _factory():
return collections.defaultdict(_factory)
これを使うと以下のようになり、KeyErrror
が発生しない世界が完成しました 。
>>> import collections
>>> def _factory():
... return collections.defaultdict(_factory)
...
>>> d = collections.defaultdict(_factory)
>>> d['a']
defaultdict(<function _factory at 0x105e93aa0>, {})
>>> d['a']['b']
defaultdict(<function _factory at 0x105e93aa0>, {})
>>> d['a']['b'][1]
defaultdict(<function _factory at 0x105e93aa0>, {})
>>> d[1][1][0][1][1][1][1][1]
defaultdict(<function _factory at 0x105e93aa0>, {})
JSONのパースでの利用例
上記の例を実世界での利用例にあてはめてみます。以下は、一人で考えてみただけなので、もっと簡単にできる綺麗な方法があるのかもしれません。ご存知の方は教えてください。
例えば以下のようなオンライン授業のコース情報のJSONがあるとします。address
のところが嫌な感じです。キーがあったりなかったりします。
{
"class":{
"id":1,
"subject":"Math",
"students":[
{
"name":"Alice",
"age":30
},
{
"name":"Bob",
"age":40,
"address":{
"country":"JP"
}
},
{
"name":"Charlie",
"age":20,
"address":{
"country":"US",
"state":"MA",
"locality":"Boston"
}
}
]
}
}
これをPythonで読み込んでみます。
In [47]: j = json.loads(s)
In [54]: for student in j["class"]["students"]:
print(student["name"])
....:
Alice
Bob
Charlie
name
はみんな持ってるので平気ですが、州の情報が欲しいと思ってstateを取得しようとすると、Bobはstateの情報を持ってないし、Aliceはそもそもaddress
というキー自体を持っていません。
In [55]: for student in j["class"]["students"]:
print(student["address"]["state"])
....:
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-55-69836c86e040> in <module>()
1 for student in j["class"]["students"]:
----> 2 print(student["address"]["state"])
3
KeyError: 'address'
dict.get(key, default_val)
を利用すればデフォルトの値を指定できますが、多段の入れ子になっているので冗長な記述になってしまいます。深さが増えたらどんどんしんどくなります。
そこでdefaultdict
です。
json.load
とjson.loads
はobject_hook
というデコード結果のdictに対してのフック処理を指定する口が用意されているので、それを利用してみましょう。こんなAPIが用意されているなんて、Pythonは素晴らしい言語ですね。
18.2. json — JSON エンコーダおよびデコーダ — Python 2.7ja1 documentation
以下のようなメソッドを定義しておいて、
def _hook(d):
return collections.defaultdict(_factory, d)
それをjson.loads
のobject_hook
に渡します。
In [75]: j2 = json.loads(s, object_hook=_hook)
In [83]: for student in j2["class"]["students"]:
....: print(student["address"]["state"])
....:
defaultdict(<function _factory at 0x10a57ccf8>, {})
defaultdict(<function _factory at 0x10a57ccf8>, {})
MA
やりました。KeyErrror
がおきません。
このままでは少し使いにくいので変換用の補助メソッドを通してあげます。使用する代替の値を指定できるようにしてあげて、デフォルトの代替値をdefault_state
という文字列としています。
In [91]: def _dd(v, alt_val="default_state"):
return alt_val if isinstance(v, collections.defaultdict) and len(v) == 0 else v
....:
In [92]: for student in j2["class"]["students"]:
print(_dd(student["address"]["state"]))
....:
default_state
default_state
MA
これで、見つからないキーがどこにあったとしても、少ない記述量でデフォルトの値を指定できるようになりました。
本当は末尾の呼び出し(上記コードでのstudent["address"]
ではなくstudent["address"]["state"]
での呼び出しタイミングという意味)であった場合にはデフォルトの値を返す、としたかったのですが、今が末尾の呼び出しの呼び出しであるかの判断ができなかったので、諦めました。なにかやり方をご存知の方は教えて下さい。
以上です。