Jaki jest najlepszy sposób na wdrożenie zagnieżdżonych słowników?


201

Mam strukturę danych, która zasadniczo odpowiada zagnieżdżonemu słownikowi. 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 utrzymanie i tworzenie tego jest dość bolesne; za każdym razem, gdy mam nowy stan / okręg / zawód, muszę tworzyć słowniki niższej warstwy za pomocą ohydnych bloków try / catch. Co więcej, muszę stworzyć irytujące zagnieżdżone iteratory, jeśli chcę przejrzeć wszystkie wartości.

Mógłbym również użyć 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 jest bardzo prosta i naturalna, ale bardziej bolesne pod względem składni jest robienie takich rzeczy, jak agregacja i przeglądanie podzbiorów słownika (np. Gdybym chciał przejść tylko stan po stanie).

Zasadniczo czasami czasem myślę o zagnieżdżonym słowniku jako o płaskim słowniku, a czasem o prawdziwej złożonej hierarchii. Mógłbym to wszystko zawrzeć w klasie, ale wygląda na to, że ktoś już to zrobił. Alternatywnie wydaje się, że mogą istnieć naprawdę eleganckie konstrukcje syntaktyczne.

Jak mogłem to zrobić lepiej?

Dodatek: Jestem tego świadomy, setdefault()ale tak naprawdę nie zapewnia czystej składni. Ponadto każdy utworzony słownik podrzędny nadal musi być setdefault()ustawiony ręcznie.

Odpowiedzi:


178

Jaki jest najlepszy sposób na implementację zagnieżdżonych słowników w Pythonie?

To zły pomysł, nie rób tego. Zamiast tego używaj zwykłego słownika i używaj dict.setdefaultgdzie apropos, więc gdy w normalnym użyciu brakuje kluczy, otrzymasz oczekiwane KeyError. Jeśli nalegasz na uzyskanie takiego zachowania, oto jak zastrzelić się w stopę:

Zaimplementuj __missing__w dictpodklasie, aby ustawić i zwrócić nową instancję.

Podejście to jest dostępne (i udokumentowane) od Pythona 2.5 i (szczególnie dla mnie cenne) wygląda dość podobnie jak zwykłe dyktowanie , zamiast brzydkiego drukowania autouaktywnionego domyślnego dykta:

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]znajduje się po lewej stronie zadania, więc nie ma tu rekurencji).

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żytkowania:

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

I 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

Krytyką tego typu kontenera jest to, że jeśli użytkownik źle wpisuje klucz, nasz kod może po cichu zawieść:

>>> vividict['new york']['queens counyt']
{}

Dodatkowo w naszych danych mielibyśmy błędnie napisane hrabstwo:

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

Udostępniamy tylko kolejną zagnieżdżoną instancję naszej klasy Vividictza każdym razem, gdy klucz jest dostępny, ale brakuje go. (Zwrócenie przypisania wartości jest przydatne, ponieważ pozwala uniknąć dodatkowego wywoływania gettera na dykcie i niestety nie możemy go zwrócić w trakcie ustawiania).

Zauważ, że są to te same semantyki co najbardziej uprzywilejowana odpowiedź, ale w połowie wierszy kodu - implementacja 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

Demonstracja użytkowania

Poniżej znajduje się tylko przykład tego, jak ten dykt można łatwo wykorzystać do stworzenia zagnieżdżonej struktury dykta w locie. To może 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 się w celu ręcznej kontroli. Ale jeśli chcesz wizualnie sprawdzić swoje dane, implementacja, __missing__aby ustawić nową instancję swojej klasy na klucz i zwrócić ją, jest znacznie lepszym rozwiązaniem.

Inne alternatywy dla kontrastu:

dict.setdefault

Chociaż pytający uważa, że ​​to nie jest czyste, uważam, że lepiej niż Vividictja sam.

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

i 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 zawiodłaby głośno i nie zaśmiecałaby 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ł przestrzegać następujących zasad:

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

Kolejną krytyką jest to, że setdefault wymaga nowej instancji, niezależnie od tego, czy jest używana, czy nie. Jednak Python (lub przynajmniej CPython) jest dość inteligentny w obsłudze nieużywanych i niereferencyjnych nowych instancji, na przykład ponownie wykorzystuje lokalizację w pamięci:

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

Auto-vivified defaultdict

Jest to ładnie wyglądająca 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 automatycznie przywróconego domyślnego nakazu zapełnionego danymi w ten sam sposób wyglądają następująco:

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

Ten wynik jest dość nieelegancki, a wyniki są dość nieczytelne. Zwykle podanym rozwiązaniem jest rekurencyjne przekształcenie z powrotem w dykt w celu ręcznej kontroli. To nietrywialne rozwiązanie pozostawia się jako ćwiczenie dla czytelnika.

Występ

Na koniec spójrzmy na wydajność. Odejmuję koszty tworzenia instancji.

>>> 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.setdefaultdziała najlepiej. Gorąco polecam go do kodu produkcyjnego, w przypadkach, gdy zależy Ci na szybkości wykonywania.

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

Wniosek

Implementowanie __missing__podklasy dictdo ustawiania i zwracania nowej instancji jest nieco trudniejsze niż alternatywy, ale ma zalety

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

a ponieważ jest mniej skomplikowany i bardziej wydajny niż modyfikowanie __getitem__, powinien być preferowany w stosunku do tej metody.

Ma jednak wady:

  • Nieprawidłowe wyszukiwania zakończą się niepowodzeniem.
  • Niepoprawne wyszukiwanie pozostanie w słowniku.

Dlatego osobiście wolę setdefaultinne rozwiązania i mam w każdej sytuacji, w której potrzebowałem tego rodzaju zachowania.


Doskonała odpowiedź! Czy istnieje sposób na określenie skończonej głębokości i typu liścia dla Vividict? Np. 3I listdla dykta dykta list, które można wypełnić d['primary']['secondary']['tertiary'].append(element). Mógłbym zdefiniować 3 różne klasy dla każdej głębokości, ale chciałbym znaleźć czystsze rozwiązanie.
Eric Duminil,

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Dzięki za komplement, ale szczerze mówiąc - nigdy nie używam __missing__- zawsze używam setdefault. Prawdopodobnie powinienem zaktualizować moje zakończenie / wstęp ...
Aaron Hall

@AaronHall Prawidłowe zachowanie polega na tym, że w razie potrzeby kod powinien utworzyć dyktando. W takim przypadku poprzez zastąpienie poprzedniej przypisanej wartości.
nehem

@AaronHall Czy możesz mi pomóc zrozumieć, co mam na myśli, The bad lookup will remain in the dictionary.gdy rozważam użycie tego rozwiązania ?. Bardzo mile widziane. Thx
nehem

@AaronHall Problem z nim nie powiedzie się, setdefaultgdy zagnieżdżony zostanie więcej niż dwa poziomy głębokości. Wygląda na to, że żadna struktura w Pythonie nie może zaoferować prawdziwego ożywienia zgodnie z opisem. Musiałem zadowolić się dwiema metodami określania: jedna dla get_nested& jedna, dla set_nestedktórych akceptuję odniesienie do dykta i listę zagnieżdżonych atrybutów.
nehem

188
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

Wynik:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

Czy ktoś ma ten problem po przejściu na Python 3.x? stackoverflow.com/questions/54622935/…
jason

@jason picklejest straszny między wersjami Pythona. Unikaj używania go do przechowywania danych, które chcesz zachować. Używaj go tylko do skrzynek i rzeczy, które możesz zrzucić i zregenerować do woli. Nie jako metoda długoterminowego przechowywania lub serializacji.
nosklo

Czego używasz do przechowywania tych obiektów? Mój obiekt automatyzacji zawiera tylko ramki danych i ciąg pand.
jason

@jason W zależności od danych lubię sqlitedo przechowywania plików JSON, csv, a nawet bazy danych.
nosklo

30

Tylko dlatego, że nie widziałem takiego małego, oto dyktando, które jest tak zagnieżdżone, jak chcesz, bez potu:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)

2
@wberry: Właściwie wszystko czego potrzebujesz to yodict = lambda: defaultdict(yodict).
martineau,

1
Zaakceptowana wersja jest podklasą dict, więc aby być w pełni równoważnym, musielibyśmy x = Vdict(a=1, b=2)pracować.
wberry

@wberry: Niezależnie od tego, co jest w zaakceptowanej odpowiedzi, bycie podklasą dictnie było wymogiem określonym przez PO, który poprosił tylko o „najlepszy sposób” ich wdrożenia - a poza tym nie powinien / nie powinien w Pythonie i tak ma to duże znaczenie.
martineau

24

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

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

i teraz my_shnazzy_dictionaryma wszystkie twoje wartości. Jeśli musisz to zrobić w locie, możesz utworzyć YAML jako ciąg i karmić go yaml.safe_load(...).


4
YAML to zdecydowanie mój wybór do wprowadzania wielu głęboko zagnieżdżonych danych (i plików konfiguracyjnych, makiet danych itp.). Jeśli OP nie chce, aby leżały wokół niego dodatkowe pliki, wystarczy użyć zwykłego ciągu Python w jakimś pliku i parsować go za pomocą YAML.
kmelvn

Dobra uwaga na temat tworzenia ciągów YAML: Byłoby to znacznie czystsze podejście niż wielokrotne używanie modułu „tempfile”.
Pete,

18

Ponieważ masz projekt schematu gwiazdy, możesz chcieć go ustrukturyzować 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 podobnego do hurtowni danych bez narzutów SQL.


14

Jeśli liczba poziomów zagnieżdżenia jest niewielka, używam collections.defaultdictdo 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

Korzystanie defaultdicttak unika dużo niechlujny setdefault(), get()itp


+1: defaultdict jest jednym z moich ulubionych dodatków do Pythona. Nigdy więcej .setdefault ()!
John Fouhy,

8

Ta funkcja zwraca zagnieżdżony słownik o dowolnej głębokości:

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

Użyj tego w ten sposób:

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"

Iteruj 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

Być może w końcu zechcesz to zrobić, aby nowe elementy nie mogły zostać dodane do dykt. Łatwo jest rekurencyjnie przekonwertować te wszystkie defaultdictna normalne dict.

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)

7

Uważam, że jest setdefaultbardzo użyteczny; 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, dlatego faktycznie aktualizujesz wartości „d ” na miejscu.

Jeśli chodzi o iterację, jestem pewien, że możesz napisać generator dość łatwo, jeśli nie istnieje on już 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)

Podoba mi się to rozwiązanie, ale kiedy próbuję: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 dostaję „niedozwolone wyrażenie dla rozszerzonego przypisania”
dfrankow

6

Jak sugerują inni, relacyjna baza danych może być dla Ciebie bardziej przydatna. Możesz użyć bazy danych sqlite3 w pamięci jako struktury danych do tworzenia tabel, a następnie ich przeszukiwania.

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ć osobne tabele dla stanów, powiatów i stanowisk.


5

collections.defaultdictmożna podzielić na podklasy, aby utworzyć zagnieżdżony dykt. Następnie dodaj do tej klasy wszelkie przydatne metody iteracji.

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

1
To jest odpowiedź najbliższa temu, czego szukałem. Ale idealnie byłyby wszelkiego rodzaju funkcje pomocnicze, np. Walk_keys () lub podobne. Dziwię się, że w standardowych bibliotekach nie ma nic takiego do zrobienia.
YGA

4

Jeśli chodzi o „nieznośne bloki try / catch”:

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

daje

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

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

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

defaultdict() jest twoim przyjacielem!

W przypadku słownika dwuwymiarowego możesz wykonać:

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

Ta odpowiedź działa tylko na trzech poziomach. W przypadku dowolnych poziomów rozważ tę odpowiedź .
Acumenus

3

Aby ułatwić iterację po zagnieżdżonym słowniku, 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
                }

Zatem jeśli masz skompilowany słownik zagnieżdżony, iteracja po 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 generować dowolny format danych, który jest dla Ciebie użyteczny.

Dlaczego używasz bloków try catch, aby odczytać drzewo? Łatwo (i prawdopodobnie bezpieczniej) jest sprawdzenie, czy klucz istnieje w nagraniu przed próbą jego odzyskania. Funkcja korzystająca z klauzul ochronnych może wyglądać następująco:

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

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

Lub, być może nieco bardziej szczegółowa, jest użycie metody get:

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

Ale dla nieco bardziej zwięzłego sposobu możesz rozważyć użycie collections.defaultdict , który jest częścią standardowej biblioteki 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']

Przyjmuję tutaj założenia dotyczące znaczenia twojej struktury danych, ale powinno być łatwo dostosować się do tego, co naprawdę chcesz zrobić.


2

I podoba mi się pomysł owijania tego w klasie i wdrażaniu __getitem__i __setitem__takie, które realizowane są 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 się zachwycić, możesz również zaimplementować coś takiego:

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

ale przede wszystkim myślę, że takie wdrożenie byłoby naprawdę fajne: D


Myślę, że to zły pomysł - nigdy nie można przewidzieć składni klawiszy. Nadal nadpisujesz getitem i setitem, ale każesz im brać krotki.
YGA

3
@YGA Prawdopodobnie masz rację, ale fajnie jest myśleć o implementacji takich mini-języków.
Aaron Maenpaa,

1

O ile Twój zestaw danych nie pozostanie dość mały, możesz rozważyć użycie relacyjnej bazy danych. Robi dokładnie to, co chcesz: ułatwia dodawanie liczb, wybieranie ich podzbiorów, a nawet agregowanie liczb według stanu, powiatu, zawodu lub dowolnej ich kombinacji.


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

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

>>>

Edycja: teraz zwracane są słowniki przy wyszukiwaniu za pomocą symboli wieloznacznych ( None), w przeciwnym razie pojedyncze wartości.


Po co zwracać listy? Wydaje się, że powinien zwrócić słownik (abyś wiedział, co oznacza każda liczba) lub sumę (ponieważ to wszystko, co naprawdę możesz zrobić z listą).
Ben Blank,

0

Mam podobną sprawę. Mam wiele przypadków, w których:

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

Ale wchodzenie na wiele poziomów. To „.get (item, {})” jest kluczem, ponieważ utworzy kolejny słownik, jeśli jeszcze go nie ma. W międzyczasie zastanawiałem się, jak lepiej sobie z tym poradzić. W tej chwili jest ich wiele

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

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

Co ma taki sam efekt, jeśli:

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

Lepszy? Chyba tak.


0

Możesz używać rekurencji w lambdas i defaultdict, nie musisz definiować 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})})})

0

Korzystałem z tej funkcji. jest bezpieczny, szybki i ł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
>>>
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.