Pythonでネストされた辞書を実装する最良の方法は何ですか?
これは悪い考えです。やらないでください。代わりに、通常の辞書を使用してdict.setdefault
where aproposを使用しますKeyError
。これにより、通常の使用法でキーが欠落している場合、期待どおりの結果が得られます。あなたがこの振る舞いを得ることを主張するならば、これは足で自分を撃つ方法です:
サブインスタンスを実装__missing__
してdict
、新しいインスタンスを設定して返します。
このアプローチはPython 2.5以降で利用可能(および文書化)であり、(特に私にとっては貴重です)autovivified defaultdictの醜い印刷の代わりに、通常のdictのようにきれいに印刷します。
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value # faster to return than dict lookup
(メモself[key]
は割り当ての左側にあるので、ここでは再帰はありません。)
あなたはいくつかのデータを持っていると言います:
data = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
使用コードは次のとおりです。
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
そして今:
>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
批判
このタイプのコンテナに対する批判は、ユーザーがキーのスペルを間違えると、コードが黙って失敗する可能性があることです。
>>> vividict['new york']['queens counyt']
{}
さらに、今度は、データのスペルが間違っている郡があります。
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36},
'queens counyt': {}}}
説明:
クラスの別のネストされたインスタンスを提供しているだけです Vividict
キーがアクセスされたが見つからない。(値の割り当てを返すことは、dictでゲッターをさらに呼び出すことを回避するので役立ちます。残念ながら、設定されているため、返すことはできません。)
これらは最も支持されている回答と同じセマンティクスですが、コードの半分の行であることに注意してください-noskloの実装:
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
使用例
以下は、このdictを使用して、ネストされたdict構造をその場で簡単に作成する方法の例にすぎません。これにより、階層ツリー構造を必要なだけ深くすばやく作成できます。
import pprint
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
d = Vividict()
d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)
どの出力:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
そして最後の行が示すように、それは美しく、手作業での検査のためにきれいに印刷されます。ただし、データを視覚的に検査したい場合は、__missing__
場合は、そのクラスの新しいインスタンスをキーに設定して返すようする方がはるかに優れたソリューションです。
対照的に、他の選択肢:
dict.setdefault
質問者はこれはきれいではないと考えていますが、私はVividict
自分よりも望ましいと思います。
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
そして今:
>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
スペルミスは騒々しく失敗し、悪い情報でデータが乱雑になることはありません。
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
さらに、ループで使用するとsetdefaultはうまく機能すると思いますが、キーに対して何を取得するのか分からないのですが、繰り返し使用すると非常に負担が大きくなり、誰もが次のことを続けたくないと思います。
d = dict()
d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})
もう1つの批判は、setdefaultが使用されているかどうかに関係なく、新しいインスタンスが必要であることです。ただし、Python(または少なくともCPython)は、未使用で参照されていない新しいインスタンスの処理についてはかなりスマートです。たとえば、メモリ内の場所を再利用します。
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
自動有効化されたdefaultdict
これは見栄えの良い実装であり、データを検査していないスクリプトでの使用は、実装と同じくらい便利です__missing__
。
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
ただし、データを検査する必要がある場合、同じ方法でデータが入力された自動ビビティ化されたdefaultdictの結果は次のようになります。
>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint;
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar':
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>,
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})
この出力は非常に洗練されておらず、結果は非常に判読できません。通常与えられる解決策は、手動で検査するために再帰的に辞書に変換することです。この重要な解決策は、読者の練習問題として残されています。
パフォーマンス
最後に、パフォーマンスを見てみましょう。インスタンス化のコストを差し引いています。
>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747
パフォーマンスに基づいてdict.setdefault
、最適に動作します。実行速度を重視する場合は、量産コードに強くお勧めします。
これをインタラクティブに使用する必要がある場合(おそらくIPythonノートブックで)、パフォーマンスはそれほど重要ではありません。その場合、出力を読みやすくするためにVividictを使用します。AutoVivificationオブジェクト(この目的のために作成されたの__getitem__
代わりにを使用)と比較すると、__missing__
はるかに優れています。
結論
新しいインスタンスを設定して返す__missing__
ようにサブクラス化dict
して実装することは、他の方法よりも少し難しいですが、
- 簡単なインスタンス化
- 簡単なデータ入力
- 簡単なデータ表示
変更するよりも複雑でなく、パフォーマンスが高いため __getitem__
その方法よりも推奨されます。
それにもかかわらず、欠点があります。
- 不正な検索は警告なしに失敗します。
- 不正な検索は辞書に残ります。
したがって、私は個人的にsetdefault
他の解決策を好み、あらゆる状況でこの種の行動が必要でした。
Vividict
か?たとえば3
、list
を入力できるリストのdict of dictですd['primary']['secondary']['tertiary'].append(element)
。深さごとに3つの異なるクラスを定義できますが、より明確な解決策を見つけたいです。