¿Por qué funcionó dict.get (key) pero no dict [key]?

17

Estoy tratando de agrupar las cadenas binarias de ciertos números en función de cuántos 1 hay en la cadena.

Esto no 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]

El diccionario esperado one_groupsdebe ser

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

Pero consigo

{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']}

Hasta ahora, lo único que ha funcionado es si uso en one_groups[x.count('1')] = one_groups.get(x.count('1')) + [x]lugar deone_groups[x.count('1')] += [x]

¿Pero por qué es así? Si recuerdo correctamente, ¿no se dict[key]supone que devuelve el valor de ese diccionario, similar a cómo dict.get(key)funciona? He visto este hilo ¿Por qué dict.get (key) en lugar de dict [key]? pero no respondió mi pregunta para este caso en particular, ya que estoy seguro de que el programa no está destinado a obtener elKeyError

También lo he intentado one_groups[x.count('1')].append(x)pero esto tampoco funciona.

SpectraXCD
fuente
8
getdevuelve Nonesi la clave no existe o algún valor predeterminado proporcionado, mientras que el operador de índice []genera un error si la clave no existe.
adnanmuttaleb
Sidenote, bin(x)[2:].rjust(4, '0')se puede simplificar a '{:0>4b}'.format(x).
wjandrea
1
Por cierto, ayuda a hacer un ejemplo reproducible mínimo . En este caso, la forma en que realiza binariesno es relevante para la pregunta, por lo que podría proporcionar su valor.
wjandrea
1
¿Responde esto a tu pregunta? dict.fromkeys todos apuntan a la misma lista
Georgy

Respuestas:

24

El problema es la mutabilidad:

one_groups = dict.fromkeys(range(5), [])- esto pasa la misma lista como valor a todas las claves . Entonces, si cambia un valor, los cambia a todos.

Es básicamente lo mismo que decir:

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

Si desea utilizar una nueva lista, debe hacerlo en un bucle, ya sea un forbucle explícito o en una comprensión dict:

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

Esta cosa se "ejecutará" [](lo que equivale a list()) para cada clave, haciendo así los valores con diferentes listas.


¿Por qué getfunciona? Porque explícitamente toma la lista actual, pero +crea una nueva lista de resultados. Y no importa si es one_groups[x.count('1')] = one_groups.get(x.count('1')) + [x]o one_groups[x.count('1')] = one_groups[x.count('1')] + [x]lo que importa es que hay +.

Sé que todo el mundo dice que a+=bes justo a=a+b, pero la implementación puede ser diferente para la optimización; en el caso de las listas, +=es solo .extendporque sabemos que queremos nuestro resultado en la variable actual, por lo que crear una nueva lista sería un desperdicio de memoria.

h4z3
fuente
Ah, sí, entendido. También recuerdo haber tenido un problema similar cuando quería crear una lista 2D usando mylist = [[] * 5] * 5y cómo lo mylist = [[] for x in range(5)] * 5habría solucionado. Solo para una aclaración rápida, según entendí, esto sucede debido a las variables que apuntan a la dirección de memoria de esa lista vacía. ¿Esto también significa que el problema no ocurriría si usara primitivas en su lugar?
SpectraXCD
1
Sí, si usó primitivas, esto lo resolverá, pero se romperá one_groups[x.count('1')] += [x]porque no puede agregar una lista a un tipo primitivo. Una mejor solución es usar defaultdict en su lugar.
Fakher Mokadem
44
específicamente, +llama __add__y devuelve un nuevo objeto, mientras que +=llama __iadd__, y no está obligado a devolver un nuevo objeto
njzk2
8

El problema es usar one_groups = dict.fromkeys(range(5), [])

(Esto pasa la misma lista como valor a todas las claves. Entonces, si cambia un valor, los cambia a todos)


Puedes usar esto en su lugar: one_groups = {i:[] for i in range(5)}

(Esta cosa "ejecutará" [] (que equivale a list ()) para cada clave, haciendo así los valores con diferentes listas.)

Hameda169
fuente
66
Tiene toda la razón, aunque una explicación sería realmente útil. Realmente no es obvio cuál es la diferencia entre las dos líneas.
Simon Fink el
Sí, es mi mal. lo siento
Hameda169
4

Esta es la ayuda sobre el fromkeysmétodo de dict .

Ayuda sobre la función incorporada de las teclas:

Método fromkeys (iterable, value = None, /) de instancia builtins.type Cree un nuevo diccionario con claves de iterable y valores establecidos en value

Eso dice que fromkeys aceptará un valor, e incluso si es invocable, lo evaluará primero y luego asignará ese valor a todas las claves dict.

Las listas son mutables en Python, por lo que asignará la misma referencia de lista vacía y un cambio los afectará a todos.

Utilice defaultdict en su lugar como sigue:

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

Esto aceptará asignaciones a claves no existentes y los valores predeterminados serán listas vacías (en este caso).

Fakher Mokadem
fuente