Pythonでネストされた辞書を実装する最良の方法は何ですか?
これは悪い考えです、やらないでください。代わりに、通常の辞書を使用dict.setdefault
して where apropos を使用するため、通常の使用でキーが欠落している場合、期待されるKeyError
. このような行動をとることを主張する場合は、次の方法で自分を撃つことができます。
サブクラスに実装__missing__
してdict
、新しいインスタンスを設定して返します。
このアプローチは Python 2.5 以降で利用可能(および文書化)であり、(特に私にとっては価値があります)自動有効化された 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 で getter を追加で呼び出す必要がなくなります。残念ながら、設定されているため、それを返すことはできません。)
これらは最も支持された回答と同じセマンティクスですが、半分のコード行であることに注意してください-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>, {})})})
この出力は非常に洗練されておらず、結果はまったく判読できません。通常、与えられる解決策は、手動検査のために再帰的に dict に戻すことです。この自明ではない解決策は、読者の演習として残されています。
パフォーマンス
最後に、パフォーマンスを見てみましょう。インスタンス化のコストを差し引いています。
>>> 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 ノートブック) でこれが必要な場合、パフォーマンスはそれほど重要ではありません。その場合、出力の読みやすさのために Vividic を使用します。AutoVivification オブジェクト (この目的のために作成された__getitem__
の代わりに使用するオブジェクト) と比較すると、はるかに優れています。__missing__
結論
__missing__
新しいインスタンスを設定して返すようにサブクラスに実装するdict
ことは、他の方法よりも少し難しくなりますが、次の利点があります。
- 簡単なインスタンス化
- 簡単なデータ作成
- 簡単なデータ表示
また、 を変更するよりも複雑ではなく、パフォーマンスが高い__getitem__
ため、その方法よりも優先する必要があります。
それにもかかわらず、欠点があります。
- 悪いルックアップは黙って失敗します。
- 不適切なルックアップは辞書に残ります。
したがって、私は個人的に他のソリューションよりも好みsetdefault
、この種の動作が必要なすべての状況で使用しています。