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.
Vividict? Np.3Ilistdla 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.