Python: defaultdict of defaultdict?

323

Czy istnieje sposób, aby mieć defaultdict(defaultdict(int))następujący kod?

for x in stuff:
    d[x.a][x.b] += x.c_int

dmusi być budowany ad hoc, w zależności od elementów x.ai x.b.

Mógłbym użyć:

for x in stuff:
    d[x.a,x.b] += x.c_int

ale wtedy nie byłbym w stanie użyć:

d.keys()
d[x.a].keys()
Jonathan
źródło
6
Zobacz podobne pytanie Jaki jest najlepszy sposób implementacji zagnieżdżonych słowników w Pythonie? . W artykule Wikipedii na temat automatycznej weryfikacji znajduje się również pewna przydatna informacja .
martineau,

Odpowiedzi:

571

Tak jak to:

defaultdict(lambda: defaultdict(int))

Argument defaultdict(w tym przypadku jest lambda: defaultdict(int)) zostanie wywołany, gdy spróbujesz uzyskać dostęp do klucza, który nie istnieje. Wartość zwracana przez nią będzie ustawiony jako nową wartość tego klucza, czyli w naszym przypadku wartość d[Key_doesnt_exist]będzie defaultdict(int).

Jeśli spróbujesz uzyskać dostęp do klucza z tego ostatniego defaultdict, tzn. Zwróci d[Key_doesnt_exist][Key_doesnt_exist]0, co jest wartością zwracaną argumentu z ostatniego defaultdict, tj int().

Mouad
źródło
7
działa świetnie! czy mógłbyś wyjaśnić uzasadnienie tej składni?
Jonathan
37
@Jathanathan: Tak, na pewno argument defaultdict(w tym przypadku jest lambda : defaultdict(int)) zostanie wywołany, gdy spróbujesz uzyskać dostęp do klucza, który nie istnieje, a jego wartość zwrotna zostanie ustawiona jako nowa wartość tego klucza, co oznacza w naszym przypadku wartość d[Key_dont_exist]będzie defaultdict(int), a jeśli spróbujesz uzyskać dostęp do klucza z tego ostatniego defaultdict, tzn d[Key_dont_exist][Key_dont_exist]. zwróci 0, co jest wartością zwracaną z argumentu ostatniego, defaultdicttj. int()Mam nadzieję, że było to pomocne.
mouad
25
Argumentem defaultdictpowinna być funkcja. defaultdict(int)jest słownikiem, natomiast lambda: defaultdict(int)funkcja, która zwraca słownik.
has2k1
27
@ has2k1 To jest niepoprawne. Argument defaultdict musi być możliwy do wywołania. Lambda jest na żądanie.
Niels Bom
2
@RickyLevi, jeśli chcesz mieć taką pracę, możesz po prostu powiedzieć: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi
51

Parametrem konstruktora defaultdict jest funkcja, która zostanie wywołana do budowania nowych elementów. Użyjmy lambda!

>>> from collections import defaultdict
>>> d = defaultdict(lambda : defaultdict(int))
>>> print d[0]
defaultdict(<type 'int'>, {})
>>> print d[0]["x"]
0

Od wersji Python 2.7 istnieje jeszcze lepsze rozwiązanie z użyciem Counter :

>>> from collections import Counter
>>> c = Counter()
>>> c["goodbye"]+=1
>>> c["and thank you"]=42
>>> c["for the fish"]-=5
>>> c
Counter({'and thank you': 42, 'goodbye': 1, 'for the fish': -5})

Niektóre funkcje dodatkowe

>>> c.most_common()[:2]
[('and thank you', 42), ('goodbye', 1)]

Aby uzyskać więcej informacji, zobacz PyMOTW - Kolekcje - Typy danych kontenerów i Dokumentacja Pythona - kolekcje

yanjost
źródło
5
Aby wypełnić ten krąg tutaj, powinieneś d = defaultdict(lambda : Counter())raczej użyć zamiast d = defaultdict(lambda : defaultdict(int))konkretnie rozwiązać problem, tak jak pierwotnie postawiono.
gumption
3
@ gumption d = defaultdict(Counter())w tym przypadku możesz po prostu nie używać lambda
Deb
3
@ Deb masz niewielki błąd - usuń wewnętrzne nawiasy, aby przekazać obiekt wywoływalny zamiast Counterobiektu. To znaczy:d = defaultdict(Counter)
Dillon Davis,
29

Uważam, że jest nieco bardziej elegancki w użyciu partial:

import functools
dd_int = functools.partial(defaultdict, int)
defaultdict(dd_int)

Oczywiście jest to to samo, co lambda.

Katriel
źródło
1
Częściowe jest również lepsze niż lambda, ponieważ można je stosować rekurencyjnie :) zobacz moją odpowiedź poniżej, aby uzyskać ogólną zagnieżdżoną metodę fabryczną defaultdict.
Campi
@Campi nie potrzebujesz częściowego dla aplikacji rekurencyjnych, AFAICT
Clément
10

W celach informacyjnych można zaimplementować ogólną zagnieżdżoną defaultdictmetodę fabryczną poprzez:

from collections import defaultdict
from functools import partial
from itertools import repeat


def nested_defaultdict(default_factory, depth=1):
    result = partial(defaultdict, default_factory)
    for _ in repeat(None, depth - 1):
        result = partial(defaultdict, result)
    return result()

Głębokość określa liczbę zagnieżdżonych słowników przed default_factoryużyciem typu zdefiniowanego w . Na przykład:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Campi
źródło
Czy możesz podać przykład użycia? Nie działa tak, jak się tego spodziewałem. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'rzutyKeyError: 'b'
David Marx
Hej, David, musisz zdefiniować głębokość swojego słownika, w przykładzie 3 (ponieważ zdefiniowałeś również default_factory jako słownik. Nested_defaultdict (dict, 3) będzie dla ciebie działać
Campi
To było bardzo pomocne, dzięki! Zauważyłem jedną rzecz, że tworzy to default_dict w depth=0, co może nie zawsze być pożądane, jeśli głębokość jest nieznana w momencie wywołania. Łatwo to naprawić, dodając linię if not depth: return default_factory()na górze funkcji, choć prawdopodobnie jest to bardziej eleganckie rozwiązanie.
Brendan
9

Poprzednie odpowiedzi dotyczyły sposobu tworzenia poziomów dwupoziomowych lub n-poziomowych defaultdict. W niektórych przypadkach potrzebujesz nieskończonego:

def ddict():
    return defaultdict(ddict)

Stosowanie:

>>> d = ddict()
>>> d[1]['a'][True] = 0.5
>>> d[1]['b'] = 3
>>> import pprint; pprint.pprint(d)
defaultdict(<function ddict at 0x7fcac68bf048>,
            {1: defaultdict(<function ddict at 0x7fcac68bf048>,
                            {'a': defaultdict(<function ddict at 0x7fcac68bf048>,
                                              {True: 0.5}),
                             'b': 3})})
Łaskawy
źródło
1
Uwielbiam to. To diabelnie proste, ale niezwykle przydatne. Dzięki!
rosstex
6

Inni poprawnie odpowiedzieli na twoje pytanie, jak uzyskać następujące działania:

for x in stuff:
    d[x.a][x.b] += x.c_int

Alternatywą byłoby użycie krotek do kluczy:

d = defaultdict(int)
for x in stuff:
    d[x.a,x.b] += x.c_int
    # ^^^^^^^ tuple key

Zaletą tego podejścia jest to, że jest prosty i można go łatwo rozszerzyć. Jeśli potrzebujesz mapowania o głębokości trzech poziomów, po prostu użyj krotki z trzema przedmiotami dla klucza.

Steven Rumbalski
źródło
4
To rozwiązanie oznacza, że ​​nie jest łatwo uzyskać wszystkie d [xa], ponieważ musisz introspekcji każdego klucza, aby zobaczyć, czy ma xa jako pierwszy element krotki.
Matthew Schinckel,
5
Jeśli chcesz zagnieździć 3 poziomy głębokości, po prostu zdefiniuj to jako 3 poziomy: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Matthew Schinckel