Каков наилучший способ реализации вложенных словарей? - 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 ]

187 голосов
/ 17 марта 2009
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}}}
157 голосов
/ 07 ноября 2013

Каков наилучший способ реализации вложенных словарей в Python?

Реализация __missing__ в подклассе dict для установки и возврата нового экземпляра.

Этот подход был доступен (и задокументирован) начиная с Python 2.5, и (что особенно ценно для меня) он довольно печатает, как обычный 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 всякий раз, когда к ключу обращаются, но пропускают. (Возвращение присваивания значения полезно, потому что оно позволяет избежать дополнительного вызова метода get для 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

Демонстрация использования

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

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)

Авто-оживленный defaultdict

Это аккуратная реализация, и использование в скрипте, на котором вы не проверяете данные, было бы так же полезно, как и реализация __missing__:

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Но если вам нужно проверить ваши данные, результаты автоматически оживленного дефолта по умолчанию, заполненного данными таким же образом, будут выглядеть так:

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

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

Performance

Наконец, давайте посмотрим на производительность. Я вычитаю затраты на инстанцирование.

>>> 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__, который был создан для этой цели) он намного превосходит.

Заключение * +1101 * Реализация __missing__ на подклассе dict для установки и возврата нового экземпляра немного сложнее, чем альтернативы, но имеет преимущества легкая реализация легкая совокупность данных простой просмотр данных и поскольку он менее сложен и более производителен, чем изменение __getitem__, его следует отдать предпочтению этому методу. Тем не менее, у него есть недостатки: Плохой поиск потерпит молчание. Плохой поиск останется в словаре. Таким образом, я лично предпочитаю setdefault другим решениям, и имею в каждой ситуации, где мне нужно такое поведение.

29 голосов
/ 19 сентября 2011

Только потому, что я не видел такого маленького, вот этот диктат, который становится настолько вложенным, насколько вам угодно, без пота:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)
22 голосов
/ 11 марта 2009

Вы можете создать файл YAML и прочитать его, используя PyYaml .

Шаг 1. Создайте файл YAML, "jobs.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(...).

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

Поскольку у вас есть схема типа «звезда», вы можете структурировать ее больше как реляционную таблицу, а не как словарь.

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.

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

Если количество уровней вложенности мало, я использую 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() и т. Д.

9 голосов
/ 30 марта 2014

Это функция, которая возвращает вложенный словарь произвольной глубины:

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

Используйте это так:

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

Переберите все с помощью чего-то подобного:

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

Это распечатывает:

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

Возможно, вы захотите сделать это так, чтобы новые предметы не могли быть добавлены к диктату. Легко рекурсивно преобразовать все эти defaultdict с в нормальные dict с.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)
7 голосов
/ 11 марта 2009

Я считаю 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)
6 голосов
/ 13 марта 2009

Как и предполагали другие, реляционная база данных может быть более полезной для вас. Вы можете использовать базу данных 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"'))

Это просто простой пример. Вы можете определить отдельные таблицы для штатов, округов и должностей.

5 голосов
/ 07 марта 2011

defaultdict() твой друг!

Для двумерного словаря вы можете сделать:

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

Для большего размера вы можете:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
...