Python: defaultdict of defaultdict?

323

Existe uma maneira de ter um defaultdict(defaultdict(int))para fazer o seguinte código funcionar?

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

dprecisa ser construído ad-hoc, dependendo x.ae x.belementos.

Eu poderia usar:

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

mas então eu não seria capaz de usar:

d.keys()
d[x.a].keys()
Jonathan
fonte
6
Veja uma pergunta semelhante Qual é a melhor maneira de implementar dicionários aninhados em Python? . Há também algumas informações possivelmente úteis no artigo da Wikipedia sobre Autovivificação .
martineau

Respostas:

571

Sim como isso:

defaultdict(lambda: defaultdict(int))

O argumento de a defaultdict(neste caso é lambda: defaultdict(int)) será chamado quando você tentar acessar uma chave que não existe. O valor de retorno será definido como o novo valor dessa chave, o que significa que, no nosso caso, o valor de d[Key_doesnt_exist]será defaultdict(int).

Se você tentar acessar uma chave deste último padrão, ou seja, d[Key_doesnt_exist][Key_doesnt_exist]ele retornará 0, que é o valor de retorno do argumento do último padrão, ou seja int().

mouad
fonte
7
funciona muito bem! você poderia explicar o racional por trás dessa sintaxe?
Jonathan
37
@ Jonathan: Sim, claro, o argumento de a defaultdict(neste caso é lambda : defaultdict(int)) será chamado quando você tentar acessar uma chave que não existe e o valor de retorno dela será definido como o novo valor dessa chave, que significa no nosso caso, o valor de d[Key_dont_exist]será defaultdict(int)e, se você tentar acessar uma chave deste último padrão, ou seja, d[Key_dont_exist][Key_dont_exist]ele retornará 0, que é o valor de retorno do argumento do último , defaultdictou seja int(), espero que isso tenha sido útil.
Mouad
25
O argumento para defaultdictdeve ser uma função. defaultdict(int)é um dicionário, enquanto lambda: defaultdict(int)é uma função que retorna um dicionário.
has2k1
27
@ has2k1 Isso está incorreto. O argumento para defaultdict precisa ser passível de chamada. Um lambda é exigível.
Niels Bom
2
@RickyLevi, se você quer ter que trabalhar você pode apenas dizer: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi
51

O parâmetro para o construtor defaultdict é a função que será chamada para a construção de novos elementos. Então, vamos usar um lambda!

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

Desde o Python 2.7, há uma solução ainda melhor usando o 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})

Alguns recursos de bônus

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

Para obter mais informações, consulte PyMOTW - Coleções - tipos de dados Container e documentação do Python - coleções

yanjost
fonte
5
Apenas para completar o círculo aqui, você gostaria de usar, em d = defaultdict(lambda : Counter())vez de d = defaultdict(lambda : defaultdict(int))abordar especificamente o problema como originalmente colocado.
gumption
3
@gumption você pode simplesmente usar d = defaultdict(Counter())sem necessidade de um lambda neste caso
Deb
3
@Deb, você tem um pequeno erro - remova os parênteses internos para passar um chamar em vez de um Counterobjeto. Ou seja:d = defaultdict(Counter)
Dillon Davis
29

Acho um pouco mais elegante de usar partial:

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

Claro, isso é o mesmo que um lambda.

Katriel
fonte
1
Parcial também é melhor que o lambda aqui porque pode ser aplicado recursivamente :) veja minha resposta abaixo para obter um método genérico de fábrica defaultdict aninhado.
Campi
@ Campi você não precisa de aplicativos parciais recursivos, AFAICT
Clément
10

Para referência, é possível implementar um defaultdictmétodo genérico de fábrica aninhada através de:

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

A profundidade define o número de dicionário aninhado antes que o tipo definido em default_factoryseja usado. Por exemplo:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Campi
fonte
Você pode dar um exemplo de uso? Não está funcionando como eu esperava. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'jogaKeyError: 'b'
David Marx
Hey David, você precisa definir a profundidade de seu dicionário, no seu exemplo 3 (como você definiu o default_factory ser um dicionário muito nested_defaultdict (dict, 3) irá trabalhar para você..
Campi
Isso foi super útil, obrigado! Uma coisa que notei é que isso cria um default_dict at depth=0, que nem sempre é desejado se a profundidade for desconhecida no momento da chamada. Facilmente corrigível, adicionando uma linha if not depth: return default_factory(), na parte superior da função, embora haja provavelmente uma solução mais elegante.
Brendan
9

As respostas anteriores abordaram como criar dois níveis ou n níveis defaultdict. Em alguns casos, você deseja um infinito:

def ddict():
    return defaultdict(ddict)

Uso:

>>> 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})})
Clemente
fonte
1
Eu amo isto. É diabolicamente simples, mas incrivelmente útil. Obrigado!
rosstex
6

Outros responderam corretamente à sua pergunta de como fazer o seguinte:

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

Uma alternativa seria usar tuplas para chaves:

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

O bom dessa abordagem é que ela é simples e pode ser facilmente expandida. Se você precisar mapear três níveis de profundidade, use uma tupla de três itens para a chave.

Steven Rumbalski
fonte
4
Essa solução significa que não é simples obter todos os d [xa], pois é necessário examinar cada chave para ver se ela tem xa como o primeiro elemento da tupla.
Matthew Schinckel
5
Se você queria nidificação 3 níveis de profundidade, em seguida, basta defini-lo como 3 níveis: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Matthew Schinckel