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.setdefault
gdzie 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 dict
podklasie, 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 Vividict
za 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ż Vividict
ja 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.setdefault
dział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 dict
do 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ę setdefault
inne rozwiązania i mam w każdej sytuacji, w której potrzebowałem tego rodzaju zachowania.
Vividict
? Np.3
Ilist
dla 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.d['primary']['secondary'].setdefault('tertiary', []).append('element')
- ?? Dzięki za komplement, ale szczerze mówiąc - nigdy nie używam__missing__
- zawsze używamsetdefault
. Prawdopodobnie powinienem zaktualizować moje zakończenie / wstęp ...The bad lookup will remain in the dictionary.
gdy rozważam użycie tego rozwiązania ?. Bardzo mile widziane. Thxsetdefault
gdy 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 dlaget_nested
& jedna, dlaset_nested
których akceptuję odniesienie do dykta i listę zagnieżdżonych atrybutów.Testowanie:
Wynik:
źródło
pickle
jest 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.sqlite
do przechowywania plików JSON, csv, a nawet bazy danych.Tylko dlatego, że nie widziałem takiego małego, oto dyktando, które jest tak zagnieżdżone, jak chcesz, bez potu:
źródło
yodict = lambda: defaultdict(yodict)
.dict
, więc aby być w pełni równoważnym, musielibyśmyx = Vdict(a=1, b=2)
pracować.dict
nie 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.Możesz utworzyć plik YAML i odczytać go za pomocą PyYaml .
Krok 1: Utwórz plik YAML „zatrudnienie.yml”:
Krok 2: Przeczytaj w Pythonie
i teraz
my_shnazzy_dictionary
ma wszystkie twoje wartości. Jeśli musisz to zrobić w locie, możesz utworzyć YAML jako ciąg i karmić goyaml.safe_load(...)
.źródło
Ponieważ masz projekt schematu gwiazdy, możesz chcieć go ustrukturyzować bardziej jak tabelę relacyjną, a mniej jak słownik.
Tego rodzaju rzeczy mogą przejść długą drogę do stworzenia projektu podobnego do hurtowni danych bez narzutów SQL.
źródło
Jeśli liczba poziomów zagnieżdżenia jest niewielka, używam
collections.defaultdict
do tego:Korzystanie
defaultdict
tak unika dużo niechlujnysetdefault()
,get()
itpźródło
Ta funkcja zwraca zagnieżdżony słownik o dowolnej głębokości:
Użyj tego w ten sposób:
Iteruj przez wszystko z czymś takim:
To drukuje:
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
defaultdict
na normalnedict
.źródło
Uważam, że jest
setdefault
bardzo użyteczny; Sprawdza, czy klucz jest obecny i dodaje go, jeśli nie: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:
źródło
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.
To tylko prosty przykład. Można zdefiniować osobne tabele dla stanów, powiatów i stanowisk.
źródło
collections.defaultdict
można podzielić na podklasy, aby utworzyć zagnieżdżony dykt. Następnie dodaj do tej klasy wszelkie przydatne metody iteracji.źródło
Jeśli chodzi o „nieznośne bloki try / catch”:
daje
Możesz użyć tego do konwersji z płaskiego formatu słownika na format strukturalny:
źródło
Możesz użyć Addict: https://github.com/mewwts/addict
źródło
defaultdict()
jest twoim przyjacielem!W przypadku słownika dwuwymiarowego możesz wykonać:
Aby uzyskać więcej wymiarów, możesz:
źródło
Aby ułatwić iterację po zagnieżdżonym słowniku, dlaczego nie napisać prostego generatora?
Zatem jeśli masz skompilowany słownik zagnieżdżony, iteracja po nim staje się prosta:
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:
Lub, być może nieco bardziej szczegółowa, jest użycie metody get:
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.
Przyjmuję tutaj założenia dotyczące znaczenia twojej struktury danych, ale powinno być łatwo dostosować się do tego, co naprawdę chcesz zrobić.
źródło
I podoba mi się pomysł owijania tego w klasie i wdrażaniu
__getitem__
i__setitem__
takie, które realizowane są prosty język zapytań:Jeśli chcesz się zachwycić, możesz również zaimplementować coś takiego:
ale przede wszystkim myślę, że takie wdrożenie byłoby naprawdę fajne: D
źródło
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.
źródło
Przykład:
Edycja: teraz zwracane są słowniki przy wyszukiwaniu za pomocą symboli wieloznacznych (
None
), w przeciwnym razie pojedyncze wartości.źródło
Mam podobną sprawę. Mam wiele przypadków, w których:
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
Zamiast tego zrobiłem:
Co ma taki sam efekt, jeśli:
Lepszy? Chyba tak.
źródło
Możesz używać rekurencji w lambdas i defaultdict, nie musisz definiować nazw:
Oto przykład:
źródło
Korzystałem z tej funkcji. jest bezpieczny, szybki i łatwy w utrzymaniu.
Przykład:
źródło