Vários níveis de 'collection.defaultdict' em Python

176

Graças a ótimas pessoas no SO, descobri as possibilidades oferecidas por collections.defaultdict, principalmente em legibilidade e velocidade. Eu os coloquei para uso com sucesso.

Agora eu gostaria de implementar três níveis de dicionários, sendo os dois principais defaultdicte o mais baixo int. Não encontro a maneira apropriada de fazer isso. Aqui está a minha tentativa:

from collections import defaultdict
d = defaultdict(defaultdict)
a = [("key1", {"a1":22, "a2":33}),
     ("key2", {"a1":32, "a2":55}),
     ("key3", {"a1":43, "a2":44})]
for i in a:
    d[i[0]] = i[1]

Agora isso funciona, mas o seguinte, que é o comportamento desejado, não funciona:

d["key4"]["a1"] + 1

Suspeito que deveria ter declarado em algum lugar que o segundo nível defaultdicté do tipo int, mas não encontrei onde ou como fazê-lo.

A razão pela qual estou usando defaultdictem primeiro lugar é evitar ter que inicializar o dicionário para cada nova chave.

Alguma sugestão mais elegante?

Obrigado pythoneers!

Morlock
fonte

Respostas:

341

Usar:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

Isso criará um novo defaultdict(int)sempre que uma nova chave for acessada d.

interjay
fonte
2
O único problema é que não vai decolar, o significado multiprocessingé infeliz em enviá-las para frente e para trás.
Noah
19
@Noah: Ele decapará se você usar uma função nomeada no nível do módulo em vez de uma lambda.
interjay 27/03
4
@ ScienceFriction Alguma coisa específica que você precise de ajuda? Quando d[new_key]é acessado, ele chama o lambda que criará um novo defaultdict(int). E quando d[existing_key][new_key2]for acessado, um novo intserá criado.
interjay
11
Isso é incrível. Parece que renovo meus votos conjugais para Python diariamente.
MVChr #
3
Procurando mais detalhes sobre o uso desse método multiprocessinge o que é uma função no nível do módulo nomeado? Esta pergunta segue.
22915 Cecilia
32

Outra maneira de criar um padrão padrão aninhado e selecionável é usar um objeto parcial em vez de um lambda:

from functools import partial
...
d = defaultdict(partial(defaultdict, int))

Isso funcionará porque a classe defaultdict é acessível globalmente no nível do módulo:

"Você não pode selecionar um objeto parcial, a menos que a função [ou, neste caso, classe] que ele agrupa esteja acessível globalmente ... sob seu __name__ (dentro de seu __module__)" - Pickling wrapped parcialmente as funções

Nathaniel Gentile
fonte
12

Olhe para a resposta de nosklo aqui para uma solução mais geral.

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

Teste:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Resultado:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
milhas82
fonte
Obrigado pelo link @ miles82 (e pela edição, @voyager). Quão pitonesca e segura é essa abordagem?
quer
2
Infelizmente, esta solução não preserva a parte mais útil do comando default, que é o poder de escrever algo como D ['chave'] + = 1 sem se preocupar com a existência da chave. Essa é a principal característica para a qual uso o defaultdict ... mas posso imaginar que dicionários de aprofundamento dinâmico também são bastante úteis.
rschwieb
2
@rschwieb, você pode adicionar o poder de escrever + = 1 adicionando o método add .
Spazm
5

Conforme a solicitação de @ rschwieb D['key'] += 1, podemos expandir o anterior substituindo a adição pela definição do __add__método, para fazer com que ele se comporte mais como umcollections.Counter()

Primeiro __missing__será chamado para criar um novo valor vazio, que será passado para __add__. Testamos o valor, contando com valores vazios a serem False.

Consulte emulando tipos numéricos para obter mais informações sobre substituição.

from numbers import Number


class autovivify(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition for numeric types when self is empty """
        if not self and isinstance(x, Number):
            return x
        raise ValueError

    def __sub__(self, x):
        if not self and isinstance(x, Number):
            return -1 * x
        raise ValueError

Exemplos:

>>> import autovivify
>>> a = autovivify.autovivify()
>>> a
{}
>>> a[2]
{}
>>> a
{2: {}}
>>> a[4] += 1
>>> a[5][3][2] -= 1
>>> a
{2: {}, 4: 1, 5: {3: {2: -1}}}

Em vez de verificar o argumento é um número (muito não-python, amirite!), Poderíamos apenas fornecer um valor 0 padrão e, em seguida, tentar a operação:

class av2(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition when self is empty """
        if not self:
            return 0 + x
        raise ValueError

    def __sub__(self, x):
        """ override subtraction when self is empty """
        if not self:
            return 0 - x
        raise ValueError
spazm
fonte
eles devem gerar NotImplemented em vez de ValueError?
Spazm
5

Tarde para a festa, mas por profundidade arbitrária eu acabei de fazer algo assim:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

O truque aqui é basicamente tornar a DeepDictinstância em si uma fábrica válida para a construção de valores ausentes. Agora podemos fazer coisas como

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9
Rad Haring
fonte
1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

Sem erros novamente. Não importa quantos níveis estejam aninhados. pop nenhum erro também

dd = DefaultDict ({"1": 333333})

ACE Fly
fonte