Jak najlepiej zaimplementować zagnieżdżone słowniki?

Mam strukturę danych, która zasadniczo odpowiada słownikowi zagnieżdżonemu. Powiedzmy, że wygląda to tak:

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

Teraz utrzymywanie i tworzenie tego jest dość bolesne; za każdym razem, gdy mam nowy stan/Hrabstwo/zawód, muszę tworzyć słowniki dolnej warstwy za pomocą nieznośnych bloków try / catch. Co więcej, muszę tworzyć irytujące zagnieżdżone Iteratory, jeśli chcę przejść przez wszystkie wartości.

Mógłbym też używać krotek jako kluczy, takich jak:

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

To sprawia, że iteracja nad wartościami bardzo proste i naturalne, ale bardziej bolesne jest składniowo robienie takich rzeczy jak agregacje i przeglądanie podzbiorów słownika (np. jeśli chcę po prostu przejść stan po stanie).

Zasadniczo, czasami chcę myśleć o słowniku zagnieżdżonym jako płaskim słowniku, a czasami chcę myśleć o nim rzeczywiście jako o złożonej hierarchii. Mógłbym to wszystko zapakować w klasę, ale wygląda na to, że ktoś już to zrobił. Alternatywnie, wydaje się, że mogą być jakieś naprawdę eleganckie konstrukcje składniowe, aby to zrobić.

Jak mógłbym to zrobić lepiej?

Dodatek: jestem świadomy setdefault(), ale tak naprawdę nie jest to czysta składnia. Ponadto, każdy słownik podrzędny, który tworzysz, musi mieć ręcznie ustawioną setdefault().

Author: martineau, 2009-03-11

20 answers

Jak najlepiej zaimplementować zagnieżdżone słowniki w Pythonie?

Zaimplementuj __missing__ na podklasie dict, aby ustawić i zwrócić nową instancję.

To podejście jest dostępne (i udokumentowane) od Pythona 2.5 i (szczególnie cenne dla mnie) ładnie drukuje tak jak normalny dict, zamiast brzydkiego drukowania autowiwifikowanego defaultdict: {[37]]}

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

(Uwaga self[key] jest po lewej stronie zadania, więc nie ma tu rekursji.)

I powiedz, że masz jakieś dane:

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}

Oto nasz kod użycia:

vividict = Vividict()
for (state, county, occupation), number in data.items():
    vividict[state][county][occupation] = number

A teraz:

>>> 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}}}

Krytyka

Krytyka tego typu kontenerów polega na tym, że jeśli użytkownik pomyli się w pisowni klucza, nasz kod może zawieść po cichu:]}
>>> vividict['new york']['queens counyt']
{}

I dodatkowo teraz mielibyśmy błędnie zapisane Hrabstwo w naszych danych:

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

Wyjaśnienie:

Dostarczamy kolejną zagnieżdżoną instancję naszej klasy Vividict, gdy klucz jest dostępny, ale brakuje. (Zwrócenie przypisania wartości jest przydatne, ponieważ unika nas dodatkowo wywoływania gettera na dict, i niestety, nie możemy go zwrócić, ponieważ jest ustawiany.)

Jest to jedna z pierwszych odpowiedzi na pytanie, w jaki sposób można je odczytać, a w jaki sposób można je odczytać.]}
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

Demonstracja użycia

Poniżej jest tylko przykład jak ten dict może być łatwo użyty do stworzenia zagnieżdżonej struktury dict na leć. Może to szybko stworzyć hierarchiczną strukturę drzewa tak głęboko,jak chcesz.

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)

Które wyjścia:

{'fizz': {'buzz': {}},
 'foo': {'bar': {}, 'baz': {}},
 'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}

I jak pokazuje ostatnia linia, ładnie drukuje pięknie i w celu ręcznej kontroli. Ale jeśli chcesz obejrzeć swoje dane, zaimplementowanie __missing__, aby ustawić nową instancję swojej klasy do klucza i zwrócić ją jest o wiele lepszym rozwiązaniem.

Inne alternatywy, dla kontrastu:

dict.setdefault

Chociaż asker uważa, że to nie jest czyste, uważam, że jest lepsze od Vividict siebie.

d = {} # or dict()
for (state, county, occupation), number in data.items():
    d.setdefault(state, {}).setdefault(county, {})[occupation] = number

A teraz:

>>> 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}}}

Błędna pisownia zawodzi głośno i nie zaśmieca naszych danych złymi informacjami:

>>> d['new york']['queens counyt']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'

Dodatkowo, myślę, że setdefault działa świetnie, gdy jest używany w pętlach i nie wiesz, co dostaniesz za klucze, ale powtarzające się użycie staje się dość uciążliwe i nie sądzę, aby ktokolwiek chciał nadążyć za: {]}

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

Kolejna krytyka jest taka, że setdefault wymaga nowej instancji niezależnie od tego, czy jest używana, czy nie. Jednak Python (a przynajmniej CPython) jest raczej inteligentny pod względem obsługi nieużywanych i nieuregulowanych nowych instancji, na przykład ponownie wykorzystuje lokalizację w pamięci: {]}

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

Automatyczny defaultdict

Jest to bardzo prosta implementacja, a użycie w skrypcie, na którym nie sprawdzasz danych, byłoby równie przydatne jak implementacja __missing__: {]}
from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Ale jeśli chcesz sprawdzić swoje dane, wyniki automatyczny defaultdict wypełniony danymi w ten sam sposób wygląda tak:

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

To wyjście jest dość nieeleganckie, a wyniki są dość nieczytelne. Zwykle podane rozwiązanie polega na rekurencyjnej konwersji z powrotem do dict w celu ręcznej kontroli. To nietrywialne rozwiązanie zostaje pozostawione jako ćwiczenie dla czytelnika.

Wydajność

[36]}wreszcie, spójrzmy na wydajność. Odejmuję koszty powstania.
>>> 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

Na podstawie wydajności, dict.setdefault działa najlepiej. Gorąco polecam go do kodu produkcyjnego, w przypadkach, w których zależy ci na szybkości wykonania.

Jeśli potrzebujesz tego do interaktywnego użytku (być może w notebooku IPython), wydajność naprawdę nie ma znaczenia - w takim przypadku wybrałbym Vividict dla czytelności wyjścia. W porównaniu do obiektu AutoVivification (który używa __getitem__ zamiast __missing__, który został do tego stworzony) jest znacznie lepszy.

Podsumowanie

Realizacja __missing__ na podklasowane dict ustawianie i zwracanie nowej instancji jest nieco trudniejsze niż alternatywy, ale ma zalety

  • łatwa instancja
  • łatwa populacja danych
  • łatwe przeglądanie danych

A ponieważ jest mniej skomplikowana i bardziej wydajna niż modyfikacja __getitem__, powinna być preferowana niż ta metoda.

Niemniej jednak ma wady:

  • złe wyszukiwanie nie powiedzie się po cichu.
  • the bad lookup will pozostań w słowniku.
Dlatego ja osobiście wolę setdefault od innych rozwiązań i mam w każdej sytuacji, w której potrzebowałem takiego zachowania.
 143
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-27 19:38:25
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

Testowanie:

a = AutoVivification()

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

print a

Wyjście:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
 185
Author: nosklo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-16 21:53:36

Tylko dlatego, że nie widziałem takiego małego, tutaj jest dict, który jest tak zagnieżdżony, jak chcesz, bez potu:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)
 28
Author: paint can,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-06 21:41:28

Możesz utworzyć plik YAML i odczytać go za pomocą PyYaml .

Krok 1: Utwórz plik YAML, " zatrudnienie.yml": {]}

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

Krok 2: przeczytaj to w Pythonie

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

A teraz my_shnazzy_dictionary ma wszystkie twoje wartości. Jeśli chcesz to zrobić w locie, możesz utworzyć YAML jako ciąg znaków i wprowadzić go do yaml.safe_load(...).

 22
Author: Pete,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-06 21:42:20

Ponieważ masz schemat Gwiazdy, możesz chcieć uporządkować go bardziej jak tabelę relacyjną, a mniej jak słownik.

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

Tego rodzaju rzeczy mogą przejść długą drogę do stworzenia projektu przypominającego hurtownię danych bez kosztów ogólnych SQL.

 17
Author: S.Lott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-11 17:29:12

Jeśli liczba poziomów zagnieżdżania jest mała, używam collections.defaultdict do tego:

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

Używanie defaultdict w ten sposób pozwala uniknąć bałaganu setdefault(), get(), itd.

 13
Author: user26294,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-06 21:45:25

Jest to funkcja, która zwraca zagnieżdżony Słownik o dowolnej głębokości:

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

Użyj go tak:

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"

Iterować przez wszystko z czymś takim:

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)

To drukuje:

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

Możesz w końcu chcieć zrobić to tak, aby nowe elementy nie mogły być dodawane do dict. Łatwo jest rekurencyjnie przekonwertować wszystkie te defaultdict s na normalne dict s.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)
 9
Author: JnBrymn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-21 18:21:41

Uważam, żesetdefault jest całkiem przydatny; sprawdza, czy klucz jest obecny i dodaje go, jeśli nie:

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

setdefault zawsze zwraca odpowiedni klucz, więc faktycznie aktualizujesz wartości 'd'.

Jeśli chodzi o iterację, jestem pewien, że można napisać generator wystarczająco łatwo, jeśli nie istnieje jeszcze w Pythonie:

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)
 7
Author: andygeers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-06 21:47:22

Jak sugerowali inni, relacyjna baza danych może być dla ciebie bardziej przydatna. Możesz użyć wbudowanej w pamięć bazy danych SQLite3 jako struktury danych do tworzenia tabel, a następnie odpytywania ich.

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"'))
To tylko prosty przykład. Można zdefiniować oddzielne tabele dla Stanów, powiatów i tytułów zadań.
 6
Author: Roberto Bonvallet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-13 20:24:47

collections.defaultdict może być podklasowany, aby utworzyć zagnieżdżony dict. Następnie dodaj dowolne użyteczne metody iteracji do tej klasy.

>>> 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)
 5
Author: A. Coady,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-06 21:43:06

defaultdict() to twój przyjaciel!

Dla dwuwymiarowego słownika możesz zrobić:

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

Aby uzyskać więcej wymiarów, możesz:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
 5
Author: Paula,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-03 21:59:48

Co do "nieznośnych klocków try/catch":

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

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

Możesz użyć tego do konwersji z formatu płaskiego słownika do formatu strukturalnego:

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
 4
Author: vartec,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-11 17:30:56

Dla łatwego iterowania nad zagnieżdżonym słownikiem, dlaczego nie napisać prostego generatora?

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
                }

Więc jeśli masz skompilowany słownik zagnieżdżony, iteracja nad nim staje się prosta:

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

Oczywiście twój generator może uzyskać dowolny format danych, który jest dla Ciebie przydatny.

Dlaczego używasz bloków try catch, aby odczytać drzewo? Jest wystarczająco łatwo (i prawdopodobnie bezpieczniej) sprawdzić, czy klucz istnieje w dict, zanim spróbuje go odzyskać. Funkcja wykorzystująca guard klauzule mogą wyglądać tak:

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

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

Lub, być może nieco gadatliwą metodą, jest użycie metody get:

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

Ale w bardziej zwięzły sposób, możesz chcieć spojrzeć na użycie kolekcji.defaultdict , który jest częścią biblioteki standardowej od Pythona 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']

Robię założenia co do znaczenia struktury danych tutaj, ale powinno być łatwo dostosować się do tego, co rzeczywiście chcesz zrobić.

 3
Author: SpoonMeiser,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-11 20:38:52

Możesz użyć 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}}}}}
 3
Author: JnBrymn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-21 18:50:09

Podoba mi się pomysł owinięcia tego w klasę i zaimplementowania __getitem__ i __setitem__ w taki sposób, że zaimplementowali prosty język zapytań:

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

Jeśli chcesz nabrać fantazji, możesz również zaimplementować coś w stylu:

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

Ale przede wszystkim myślę, że takie coś byłoby naprawdę fajne w realizacji: d

 2
Author: Aaron Maenpaa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-06 21:48:21

O ile twój zbiór danych nie będzie dość mały, warto rozważyć użycie relacyjnej bazy danych. Zrobi dokładnie to, co chcesz: ułatwi dodawanie liczeń, wybieranie podzbiorów liczeń, a nawet zagregowanie liczeń według stanu, hrabstwa, zawodu lub dowolnej ich kombinacji.

 1
Author: allyourcode,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-11 20:30:20
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

Przykład:

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

>>>

Edit: TERAZ Zwraca słowniki przy pytaniu z dzikimi kartami (None) i pojedynczymi wartościami w przeciwnym razie.

 1
Author: Markus Jarderot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-11 21:44:27

Możesz użyć rekurencji w lambda i defaultdict, bez potrzeby definiowania nazw:

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

Oto przykład:

>>> 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
Author: topkara,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-09 04:25:21

Mam coś podobnego. Mam wiele spraw, w których robię:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict
/ Align = "left" / To".get(item, {})" to jest klucz, ponieważ utworzy kolejny słownik, jeśli go już nie ma. / Align = "left" / tak lepiej. W tej chwili jest dużo
value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

Więc zamiast tego zrobiłem:

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

Który ma taki sam efekt jeśli to zrobisz:

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')
Lepiej? Tak myślę.
 0
Author: uzi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-19 18:47:35

Używałem tej funkcji. jest bezpieczny, szybki, łatwy w utrzymaniu.

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)

Przykład:

>>> 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
>>>
 0
Author: Yuda Prawira,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-23 14:27:01