Por que dict.get (key) funcionou, mas não dict [key]?

17

Estou tentando agrupar as cadeias binárias de certos números com base em quantos 1s existem na cadeia.

Isso não funciona:

s = "0 1 3 7 8 9 11 15"
numbers = map(int, s.split())
binaries = [bin(x)[2:].rjust(4, '0') for x in numbers]

one_groups = dict.fromkeys(range(5), [])
for x in binaries:
    one_groups[x.count('1')] += [x]

O dicionário esperado one_groupsprecisa ser

{0: ['0000'], 
 1: ['0001', '1000'], 
 2: ['0011', '1001'], 
 3: ['0111', '1011'], 
 4: ['1111']}

Mas eu entendo

{0: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 1: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 2: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 3: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 4: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111']}

Até agora, a única coisa que funcionou é se eu usar em one_groups[x.count('1')] = one_groups.get(x.count('1')) + [x]vez deone_groups[x.count('1')] += [x]

Mas porque é isso? Se bem me lembro, não dict[key]deveria retornar o valor desse dicionário, semelhante a como dict.get(key)funciona? Eu já vi esse tópico Por que dict.get (key) em vez de dict [key]? mas não respondeu à minha pergunta para esse caso em particular, pois tenho certeza de que o programa não pretende obter oKeyError

Eu também tentei, one_groups[x.count('1')].append(x)mas isso também não funciona.

SpectraXCD
fonte
8
getretorne Nonese a chave não existir ou qualquer valor padrão fornecido, enquanto o operador de índice []gerará um erro se a chave não existir.
precisa saber é o seguinte
Sidenote, bin(x)[2:].rjust(4, '0')pode ser simplificado para '{:0>4b}'.format(x).
Wjandrea
11
BTW, ajuda a criar um exemplo reproduzível mínimo . Nesse caso, a forma como você faz binariesnão é relevante para a pergunta, portanto, você pode apenas fornecer seu valor.
Wjandrea
11
Isso responde sua pergunta? dict.fromkeys apontam para mesma lista
Georgy

Respostas:

24

O problema é mutabilidade:

one_groups = dict.fromkeys(range(5), [])- isso passa a mesma lista que o valor para todas as chaves . Portanto, se você alterar um valor, alterará todos eles.

É basicamente o mesmo que dizer:

tmp = []
one_groups = dict.fromkeys(range(5), tmp)
del tmp

Se você quiser usar uma nova lista, precisará fazê-lo em um loop - um forloop explícito ou em uma compreensão de ditado:

one_groups = {key: [] for key in range(5)}

Essa coisa será "executada" [](que é igual a list()) para cada chave, criando os valores com listas diferentes.


Por que getfunciona? Porque você pega explicitamente a lista atual, mas +cria uma nova lista de resultados. E não importa se é one_groups[x.count('1')] = one_groups.get(x.count('1')) + [x]ou one_groups[x.count('1')] = one_groups[x.count('1')] + [x]- o que importa é que existe +.

Eu sei como todo mundo diz que a+=bé justo a=a+b, mas a implementação pode ser diferente para otimização - no caso de listas, +=é apenas .extendporque sabemos que queremos nosso resultado na variável atual, portanto, criar uma nova lista seria desperdício de memória.

h4z3
fonte
Ah, sim, entendi. Também me lembro de ter um problema semelhante quando quis criar uma lista 2D usando mylist = [[] * 5] * 5e como a mylist = [[] for x in range(5)] * 5teria corrigido. Apenas para esclarecimentos rápidos, pelo que entendi, isso acontece devido às variáveis ​​que apontam para o endereço de memória dessa lista vazia. Isso também significa que o problema não ocorreria se eu usasse primitivas?
SpectraXCD
11
Sim, se você usou primitivas, isso a resolverá, mas será interrompido one_groups[x.count('1')] += [x]porque você não pode adicionar uma lista a um tipo primitivo. Uma solução melhor é usar o defaultdict.
Fakher Mokadem 4/11/19
4
Especificamente, +chamadas __add__e retorna um novo objeto, enquanto +=as chamadas __iadd__, e não é necessário para retornar um novo objeto
njzk2
8

O problema está usando one_groups = dict.fromkeys(range(5), [])

(Isso passa a mesma lista que o valor para todas as chaves. Portanto, se você alterar um valor, alterará todas elas)


Você pode usar isso: one_groups = {i:[] for i in range(5)}

(Essa coisa "executará" [] (que é igual a list ()) para cada chave, tornando os valores com listas diferentes.)

Hameda169
fonte
6
Você está absolutamente certo, embora uma explicação seja realmente útil. Realmente não é óbvio qual é a diferença entre as duas linhas.
Simon Fink
Sim, é ruim. desculpe
Hameda169
4

Esta é a ajuda no fromkeysmétodo do dict .

Ajuda na função interna fromkeys:

fromkeys (iterable, value = None, /) método da instância builtins.type Crie um novo dicionário com chaves de iterable e valores configurados como value

Isso indica que fromkeys aceitará um valor e, mesmo que seja possível chamar, avaliará primeiro e depois atribuirá esse valor a todas as chaves dict.

As listas são mutáveis ​​no Python e, portanto, atribuirá a mesma referência de lista vazia e uma alteração afetará todas elas.

Use defaultdict em vez disso:

>>> from collections import defaultdict
>>> one_groups = defaultdict(list)
>>> for x in binaries:
      one_groups[x.count('1')] += [x]
>>> one_groups = dict(one_groups) # to stop default dict behavior

Isso aceitará atribuições para chaves não existentes e os valores serão padronizados para listas vazias (nesse caso).

Fakher Mokadem
fonte