1. ホーム
  2. パイソン

[解決済み】ネストされた辞書を実装するのに最適な方法は?

2022-05-10 14:59:23

質問

基本的にネストされた辞書に相当するデータ構造を持っています。それは次のように見えるとしましょう。

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

新しい州、郡、職業ができるたびに、面倒な try/catch ブロックで下位レイヤーの辞書を作成しなければなりません。さらに、すべての値を調べたい場合は、厄介なネストされたイテレータを作成する必要があります。

私はまた、このようにキーとしてタプルを使用することができました。

{('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}

これは、値の反復処理を非常にシンプルかつ自然に行いますが、集約や辞書のサブセットを見るようなことをするには、構文的に難しくなります (例えば、州ごとに移動したい場合など)。

基本的に、入れ子になった辞書をフラットな辞書として考えたいこともあれば、複雑な階層構造として考えたいこともあります。これをすべてクラスでラップすることもできますが、誰かがすでにこれをやっているような気がします。あるいは、これを行うための本当にエレガントな構文がいくつかあるように思えます。

どうすればこれをよりよくできるでしょうか?

追記:私が意識しているのは setdefault() を意識していますが、これではきれいな構文になりません。 また、作成した各サブディクショナリは、やはり setdefault() を手動で設定する必要があります。

どのように解決するのですか?

Pythonでネストされた辞書を実装するための最良の方法は何ですか?

これは悪い考えです、やらないでください。代わりに、通常の辞書を使用し dict.setdefault を使うようにします。そうすれば、通常の使い方でキーが欠落したときに、期待通りの KeyError . もしこの動作にこだわるのであれば、ここで自分の足を撃つ方法を紹介します。

実装 __missing__ の上に dict のサブクラスで新しいインスタンスを設定し、返します。

このアプローチでは、これまで (そして文書化された) は Python 2.5 以降、そして (私にとって特に価値のある) それはかなり普通のディクショナリのように表示されます。 のように印刷されます。

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', {})

もう一つの批判は、setdefaultが使われているかどうかに関わらず新しいインスタンスを必要とすることです。しかし、Python(あるいは少なくともCPython)は未使用で参照されない新しいインスタンスの処理についてはかなりスマートで、例えばメモリ内の場所を再利用します。

>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)

自動生成されたデフォルトディクショナリ

この実装は見た目がきれいで、データを検査しないスクリプトで使用する場合は __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ノートブックで、おそらく)、パフォーマンスは本当に重要ではありません - その場合、私は出力の読みやすさのためにVividictを使用します。AutoVivificationオブジェクトと比べると(これは __getitem__ の代わりに __missing__ の代わりに、この目的のために作られたを使用すると、はるかに優れています。

結論

実装方法 __missing__ をサブクラス化した dict に新しいインスタンスをセットして返すことは、他の方法より少し難しいですが、次のような利点があります。

  • インスタンス化が容易
  • 簡単なデータ投入
  • 簡単なデータ閲覧

を変更するよりも複雑でなく、パフォーマンスも高いので、この方法は __getitem__ を修正するよりも複雑ではなく、パフォーマンスも良いので、その方法よりも優先されるべきです。

とはいえ、欠点もある。

  • 不正なルックアップは黙って失敗します。
  • 不正なルックアップは辞書に残ります。

したがって、私は個人的に setdefault を好みますし、私がこの種の動作を必要としたすべての状況でそうしてきました。