LoginSignup
15

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-03-31

みんな大好き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.Counter1を検討することをお勧めします。

入れ子の辞書で利用する

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

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

{  
  "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.loadjson.loadsobject_hookというデコード結果のdictに対してのフック処理を指定する口が用意されているので、それを利用してみましょう。こんなAPIが用意されているなんて、Pythonは素晴らしい言語ですね。
18.2. json — JSON エンコーダおよびデコーダ — Python 2.7ja1 documentation

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

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

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

以上です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15