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.