217

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

{'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()手動で設定する必要があります。

4

21 に答える 21

198

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、この種の動作が必要なすべての状況で使用しています。

于 2013-11-07T06:53:24.557 に答える
189
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

テスト:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

出力:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
于 2009-03-16T21:53:36.700 に答える
33

これほど小さいものを見たことがないという理由だけで、ここにあなたが好きなようにネストされる辞書があります。

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)
于 2011-09-19T19:51:07.967 に答える
26

YAML ファイルを作成し、 PyYamlを使用して読み込むことができます。

ステップ 1: YAML ファイル「employment.yml」を作成します。

new jersey:
  mercer county:
    pumbers: 3
    programmers: 81
  middlesex county:
    salesmen: 62
    programmers: 81
new york:
  queens county:
    plumbers: 9
    salesmen: 36

ステップ 2: Python で読む

import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()

そして今my_shnazzy_dictionary、あなたのすべての価値を持っています。これをオンザフライで行う必要がある場合は、YAML を文字列として作成し、それを にフィードできますyaml.safe_load(...)

于 2009-03-11T20:08:17.090 に答える
19

スタースキーマの設計があるので、辞書のようではなく、リレーショナルテーブルのように構造化することをお勧めします。

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

この種のことは、SQLのオーバーヘッドなしでデータウェアハウスのような設計を作成するのに大いに役立ちます。

于 2009-03-11T17:29:12.700 に答える
16

ネスト レベルの数が少ない場合は、次のように使用collections.defaultdictします。

from collections import defaultdict

def nested_dict_factory(): 
  return defaultdict(int)
def nested_dict_factory2(): 
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

このdefaultdictように使用すると、面倒な 、 などを避けることができsetdefault()ますget()

于 2009-03-11T20:02:11.053 に答える
7

setdefaultとても便利だと思います。キーが存在するかどうかをチェックし、存在しない場合は追加します。

d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3

setdefault常に関連するキーを返すため、実際には' d'の値をその場で更新しています。

反復に関しては、Pythonにジェネレーターがまだ存在しない場合でも、ジェネレーターを簡単に作成できると確信しています。

def iterateStates(d):
    # Let's count up the total number of "plumbers" / "dentists" / etc.
    # across all counties and states
    job_totals = {}

    # I guess this is the annoying nested stuff you were talking about?
    for (state, counties) in d.iteritems():
        for (county, jobs) in counties.iteritems():
            for (job, num) in jobs.iteritems():
                # If job isn't already in job_totals, default it to zero
                job_totals[job] = job_totals.get(job, 0) + num

    # Now return an iterator of (job, number) tuples
    return job_totals.iteritems()

# Display all jobs
for (job, num) in iterateStates(d):
    print "There are %d %s in total" % (job, num)
于 2009-03-11T17:14:19.960 に答える
7

他の人が示唆しているように、リレーショナル データベースの方が便利かもしれません。インメモリ sqlite3 データベースをデータ構造として使用してテーブルを作成し、クエリを実行できます。

import sqlite3

c = sqlite3.Connection(':memory:')
c.execute('CREATE TABLE jobs (state, county, title, count)')

c.executemany('insert into jobs values (?, ?, ?, ?)', [
    ('New Jersey', 'Mercer County',    'Programmers', 81),
    ('New Jersey', 'Mercer County',    'Plumbers',     3),
    ('New Jersey', 'Middlesex County', 'Programmers', 81),
    ('New Jersey', 'Middlesex County', 'Salesmen',    62),
    ('New York',   'Queens County',    'Salesmen',    36),
    ('New York',   'Queens County',    'Plumbers',     9),
])

# some example queries
print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"'))
print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"'))

これは簡単な例です。州、郡、役職ごとに個別のテーブルを定義できます。

于 2009-03-13T20:24:47.303 に答える
5

collections.defaultdictサブクラス化して、ネストされたdictを作成できます。次に、そのクラスに有用な反復メソッドを追加します。

>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
    def __init__(self):
        defaultdict.__init__(self, nesteddict)
    def walk(self):
        for key, value in self.iteritems():
            if isinstance(value, nesteddict):
                for tup in value.walk():
                    yield (key,) + tup
            else:
                yield key, value


>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
    print tup


('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)
于 2009-03-12T06:27:52.437 に答える
4

defaultdict()あなたの友だちです!

2次元辞書の場合、次のことができます。

d = defaultdict(defaultdict)
d[1][2] = 3

より多くの次元については、次のことができます。

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
于 2011-03-07T16:48:34.257 に答える
4

「不快なトライ/キャッチブロック」について:

d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d

収量

{'key': {'inner key': {'inner inner key': 'value'}}}

これを使用して、フラット辞書形式から構造化形式に変換できます。

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

for (k1,k2,k3), v in fd.iteritems():
    d.setdefault(k1, {}).setdefault(k2, {})[k3] = v
于 2009-03-11T17:17:40.263 に答える
4

Addict を使用できます: https://github.com/mewwts/addict

>>> from addict import Dict
>>> my_new_shiny_dict = Dict()
>>> my_new_shiny_dict.a.b.c.d.e = 2
>>> my_new_shiny_dict
{'a': {'b': {'c': {'d': {'e': 2}}}}}
于 2016-01-21T18:50:09.343 に答える
3

ネストされた辞書を簡単に反復するには、単純なジェネレーターを作成してみませんか?

def each_job(my_dict):
    for state, a in my_dict.items():
        for county, b in a.items():
            for job, value in b.items():
                yield {
                    'state'  : state,
                    'county' : county,
                    'job'    : job,
                    'value'  : value
                }

したがって、コンパイル済みのネストされた辞書がある場合、それを反復処理するのは簡単になります。

for r in each_job(my_dict):
    print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])

明らかに、ジェネレーターは、有用なデータ形式であれば何でも生成できます。

ツリーを読み取るために try catch ブロックを使用するのはなぜですか? キーを取得する前に、キーが dict に存在するかどうかをクエリするのは簡単です (そしておそらくより安全です)。ガード句を使用する関数は次のようになります。

if not my_dict.has_key('new jersey'):
    return False

nj_dict = my_dict['new jersey']
...

または、おそらくやや冗長な方法は、get メソッドを使用することです。

value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)

しかし、もう少し簡潔な方法として、python 2.5 以降の標準ライブラリの一部であるcollections.defaultdictの使用を検討することをお勧めします。

import collections

def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0

my_dict = collections.defaultdict(state_struct)

print my_dict['new jersey']['middlesex county']['salesmen']

ここでは、データ構造の意味について推測していますが、実際に何をしたいのかを簡単に調整できるはずです。

于 2009-03-11T20:05:32.137 に答える
2

__getitem__これをクラスにラップして実装し__setitem__、単純なクエリ言語を実装するというアイデアが好きです。

>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>

ファンシーになりたい場合は、次のようなものを実装することもできます。

>>> d['*/*/programmers']
<view which would contain 'programmers' entries>

しかし、ほとんどの場合、そのようなことを実装するのは本当に楽しいと思います:D

于 2009-03-11T17:19:27.467 に答える
1

データセットがかなり小さいままになる場合を除き、リレーショナル データベースの使用を検討することをお勧めします。カウントの追加、カウントのサブセットの選択、さらには州、郡、職業、またはこれらの任意の組み合わせごとの集計カウントを簡単に実行できます。

于 2009-03-11T20:30:20.800 に答える
1
class JobDb(object):
    def __init__(self):
        self.data = []
        self.all = set()
        self.free = []
        self.index1 = {}
        self.index2 = {}
        self.index3 = {}

    def _indices(self,(key1,key2,key3)):
        indices = self.all.copy()
        wild = False
        for index,key in ((self.index1,key1),(self.index2,key2),
                                             (self.index3,key3)):
            if key is not None:
                indices &= index.setdefault(key,set())
            else:
                wild = True
        return indices, wild

    def __getitem__(self,key):
        indices, wild = self._indices(key)
        if wild:
            return dict(self.data[i] for i in indices)
        else:
            values = [self.data[i][-1] for i in indices]
            if values:
                return values[0]

    def __setitem__(self,key,value):
        indices, wild = self._indices(key)
        if indices:
            for i in indices:
                self.data[i] = key,value
        elif wild:
            raise KeyError(k)
        else:
            if self.free:
                index = self.free.pop(0)
                self.data[index] = key,value
            else:
                index = len(self.data)
                self.data.append((key,value))
                self.all.add(index)
            self.index1.setdefault(key[0],set()).add(index)
            self.index2.setdefault(key[1],set()).add(index)
            self.index3.setdefault(key[2],set()).add(index)

    def __delitem__(self,key):
        indices,wild = self._indices(key)
        if not indices:
            raise KeyError
        self.index1[key[0]] -= indices
        self.index2[key[1]] -= indices
        self.index3[key[2]] -= indices
        self.all -= indices
        for i in indices:
            self.data[i] = None
        self.free.extend(indices)

    def __len__(self):
        return len(self.all)

    def __iter__(self):
        for key,value in self.data:
            yield key

例:

>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36

>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new york', 'queens county', 'plumbers'): 9}

>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81}

>>> db['new jersey', 'middlesex county', 'programmers']
81

>>>

編集:ワイルドカード ( None) を使用してクエリを実行すると辞書が返され、それ以外の場合は単一の値が返されるようになりました。

于 2009-03-11T18:52:03.843 に答える
0

私は似たようなことをしています。私が行う多くのケースがあります:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict

しかし、多くのレベルの深さになります。「.get(item, {})」がキーで、まだ辞書がない場合は別の辞書を作成します。その間、私はこれにうまく対処する方法を考えてきました。今のところ、たくさんあります

value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

だから代わりに、私は作った:

def dictgetter(thedict, default, *args):
  totalargs = len(args)
  for i,arg in enumerate(args):
    if i+1 == totalargs:
      thedict = thedict.get(arg, default)
    else:
      thedict = thedict.get(arg, {})
  return thedict

次の場合、同じ効果があります。

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')

より良い?そう思います。

于 2012-10-19T18:47:35.937 に答える