defaultdictを返すdefaultdictを作って、KeyErrrorの発生しない世界を作る（+JSONのパースでの利用例）


みんな大好き`collections`では大抵のものは用意されてて面白いです。

defaultdictを返すdefaultdictを作って、KeyErrrorの発生しない世界を作る
=====================================================================


`defaultdict`はキーが見つからなかった場合の挙動を設定できるdictの拡張クラスです。

[8.3. collections — 高性能なコンテナ・データ型 — Python 2.7ja1 documentation](http://docs.python.jp/2/library/collections.html?highlight=collections#collections.defaultdict)

キーの有無での振る舞いの場合分けが不要になるので、集計処理などで利用するとたいへん便利です。
使用例の転載になりますが、これが一番わかりやすいでしょう。

```
>>> 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`[^counter]を検討することをお勧めします。

[^counter]: [8.3. collections — 高性能なコンテナ・データ型 — Python 2.7ja1 documentation](http://docs.python.jp/2/library/collections.html?highlight=collections#collections.Counter)

入れ子の辞書で利用する
---------------------------

例えば、入れ子の辞書で`d['a']['b']['c']`という呼び出しをする場合、もし`d`が`defaultdict`だったとしても、`d['a']`のバリューが通常の`dict`だとそこで`KeyErrror`が起きるかもしれません。
そこで、`defaultdict`に渡す`default_factory`を以下のようにしてみます。

```python
def _factory():
    return collections.defaultdict(_factory)
```

これを使うと以下のようになり、`KeyErrror`が発生しない世界が完成しました :accept: 。

```
>>> 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`のところが嫌な感じです。キーがあったりなかったりします。

```json
{  
  "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で読み込んでみます。

```ipython-shell
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`というキー自体を持っていません。

```ipython-shell
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](http://docs.python.jp/2/library/json.html?highlight=json#json.load)

以下のようなメソッドを定義しておいて、

```
def _hook(d):
    return collections.defaultdict(_factory, d)
```

それを`json.loads`の`object_hook`に渡します。

```ipython-shell
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`という文字列としています。

```ipython-shell
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"]`での呼び出しタイミングという意味）であった場合にはデフォルトの値を返す、としたかったのですが、今が末尾の呼び出しの呼び出しであるかの判断ができなかったので、諦めました。なにかやり方をご存知の方は教えて下さい。

以上です。