Каков наилучший способ реализации вложенных словарей? - PullRequest
186 голосов
/ 11 марта 2009

У меня есть структура данных, которая по сути составляет вложенный словарь. Допустим, это выглядит так:

{'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() вручную.

Ответы [ 20 ]

5 голосов
/ 12 марта 2009

collections.defaultdict может быть разделено на подклассы для создания вложенного запроса. Затем добавьте в этот класс любые полезные итерационные методы.

>>> 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)
4 голосов
/ 11 марта 2009

Что касается "неприятных блоков try / catch":

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
3 голосов
/ 21 января 2016

Вы можете использовать Наркоман: 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}}}}}
3 голосов
/ 11 марта 2009

Для простой итерации по вложенному словарю, почему бы просто не написать простой генератор?

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 try для чтения дерева? Достаточно легко (и, вероятно, безопаснее) запросить, существует ли ключ в 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)

Но для более краткого подхода вы можете рассмотреть использование collection.defaultdict , который является частью стандартной библиотеки начиная с python 2.5.

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']

Я делаю предположения о значении вашей структуры данных здесь, но должно быть легко настроить то, что вы действительно хотите сделать.

2 голосов
/ 11 марта 2009

Мне нравится идея обернуть это в класс и реализовать __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

1 голос
/ 23 октября 2017

Я использовал эту функцию. это безопасно, быстро, легко обслуживаемо.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Пример:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
1 голос
/ 09 апреля 2016

Вы можете использовать рекурсию в lambdas и defaultdict, не нужно определять имена:

a = defaultdict((lambda f: f(f))(lambda g: lambda:defaultdict(g(g))))

Вот пример:

>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
        {'new jersey': defaultdict(<function __main__.<lambda>>,
                     {'mercer county': defaultdict(<function __main__.<lambda>>,
                                  {'plumbers': 3, 'programmers': 81}),
                      'middlesex county': defaultdict(<function __main__.<lambda>>,
                                  {'programmers': 81, 'salesmen': 62})})})
1 голос
/ 11 марта 2009
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), а в противном случае одиночные значения.

1 голос
/ 11 марта 2009

Если ваш набор данных не останется достаточно маленьким, вы можете рассмотреть возможность использования реляционной базы данных. Он будет делать именно то, что вы хотите: упростить добавление подсчетов, выбор подмножеств подсчетов и даже совокупных подсчетов по штатам, округам, роду занятий или любой их комбинации.

0 голосов
/ 19 октября 2012

У меня похожая вещь. У меня много случаев, когда я делаю:

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')

лучше? Я так думаю.

...