Lista de cambios de listas reflejados en sublistas inesperadamente
Creé una lista de listas:
>>> xs = [[1] * 4] * 3
>>> print(xs)
[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
Luego, cambié uno de los valores más internos:
>>> xs[0][0] = 5
>>> print(xs)
[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]
¿ Por qué cada primer elemento de cada sublista cambió a 5
?
Ver también:
¿Cómo clono una lista para que no cambie inesperadamente después de la asignación? para soluciones alternativas al problema
La lista de diccionarios almacena solo el último valor agregado en cada iteración para un problema análogo con una lista de dictados
¿Cómo inicializo un diccionario de listas vacías en Python? para un problema análogo con un dictado de listas
Cuando escribes, [x]*3
obtienes, esencialmente, la lista [x, x, x]
. Es decir, una lista con 3 referencias al mismo x
. Cuando modificas este single x
, es visible a través de las tres referencias al mismo:
x = [1] * 4
xs = [x] * 3
print(f"id(x): {id(x)}")
# id(x): 140560897920048
print(
f"id(xs[0]): {id(xs[0])}\n"
f"id(xs[1]): {id(xs[1])}\n"
f"id(xs[2]): {id(xs[2])}"
)
# id(xs[0]): 140560897920048
# id(xs[1]): 140560897920048
# id(xs[2]): 140560897920048
x[0] = 42
print(f"x: {x}")
# x: [42, 1, 1, 1]
print(f"xs: {xs}")
# xs: [[42, 1, 1, 1], [42, 1, 1, 1], [42, 1, 1, 1]]
Para solucionarlo, debe asegurarse de crear una nueva lista en cada posición. Una manera de hacerlo es
[[1]*4 for _ in range(3)]
que lo reevaluará [1]*4
cada vez en lugar de evaluarlo una vez y hacer 3 referencias a 1 lista.
Quizás se pregunte por qué *
no se pueden crear objetos independientes como lo hace la comprensión de listas. Esto se debe a que el operador de multiplicación *
opera sobre objetos, sin ver expresiones. Cuando usas *
multiplicar [[1] * 4]
por 3, *
solo ve la lista de 1 elemento [[1] * 4]
evaluada, no el [[1] * 4
texto de la expresión. *
No tiene idea de cómo hacer copias de ese elemento, no tiene idea de cómo reevaluar [[1] * 4]
y ni siquiera tiene idea de si desea copias y, en general, es posible que ni siquiera haya una manera de copiar el elemento.
La única opción *
que tiene es hacer nuevas referencias a la sublista existente en lugar de intentar crear nuevas sublistas. Cualquier otra cosa sería inconsistente o requeriría un rediseño importante de las decisiones fundamentales de diseño del lenguaje.
Por el contrario, una lista por comprensión reevalúa la expresión del elemento en cada iteración. [[1] * 4 for n in range(3)]
reevalúa [1] * 4
cada vez por la misma razón [x**2 for x in range(3)]
reevalúa x**2
cada vez. Cada evaluación de [1] * 4
genera una nueva lista, por lo que la comprensión de la lista hace lo que usted desea.
Por cierto, [1] * 4
tampoco copia los elementos de [1]
, pero eso no importa, ya que los números enteros son inmutables. No puedes hacer algo como 1.value = 2
convertir un 1 en un 2.
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]
Visualización en vivo usando Python Tutor:
En realidad, esto es exactamente lo que cabría esperar. Descompongamos lo que está pasando aquí:
Usted escribe
lst = [[1] * 4] * 3
Esto equivale a:
lst1 = [1]*4
lst = [lst1]*3
Esto significa lst
que es una lista con 3 elementos, todos apuntando a lst1
. Esto significa que las dos líneas siguientes son equivalentes:
lst[0][0] = 5
lst1[0] = 5
Como lst[0]
es nada más lst1
.
Para obtener el comportamiento deseado, puede utilizar una lista por comprensión:
lst = [ [1]*4 for n in range(3) ]
En este caso, la expresión se vuelve a evaluar para cada uno n
, lo que genera una lista diferente.
[[1] * 4] * 3
o incluso:
[[1, 1, 1, 1]] * 3
Crea una lista que hace referencia a la lista interna [1,1,1,1]
3 veces, no a tres copias de la lista interna, por lo que cada vez que modifique la lista (en cualquier posición), verá el cambio tres veces.
Es lo mismo que este ejemplo:
>>> inner = [1,1,1,1]
>>> outer = [inner]*3
>>> outer
[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]
>>> inner[0] = 5
>>> outer
[[5, 1, 1, 1], [5, 1, 1, 1], [5, 1, 1, 1]]
donde probablemente sea un poco menos sorprendente.
my_list = [[1]*4] * 3
crea un objeto de lista [1,1,1,1]
en la memoria y copia su referencia 3 veces. Esto equivale a obj = [1,1,1,1]; my_list = [obj]*3
. Cualquier modificación se obj
reflejará en tres lugares, dondequiera que obj
se haga referencia en la lista. La afirmación correcta sería:
my_list = [[1]*4 for _ in range(3)]
o
my_list = [[1 for __ in range(4)] for _ in range(3)]
Lo importante a tener en cuenta aquí es que el *
operador se usa principalmente para crear una lista de literales . Aunque 1
es inmutable, obj = [1]*4
seguirá creando una lista 1
repetida 4 veces para formar [1,1,1,1]
. Pero si se hace alguna referencia a un objeto inmutable, el objeto se sobrescribe con uno nuevo.
Esto significa que si lo hacemos obj[1] = 42
, noobj
será como algunos suponen. Esto también se puede verificar:[1,42,1,1]
[42,42,42,42]
>>> my_list = [1]*4
>>> my_list
[1, 1, 1, 1]
>>> id(my_list[0])
4522139440
>>> id(my_list[1]) # Same as my_list[0]
4522139440
>>> my_list[1] = 42 # Since my_list[1] is immutable, this operation overwrites my_list[1] with a new object changing its id.
>>> my_list
[1, 42, 1, 1]
>>> id(my_list[0])
4522139440
>>> id(my_list[1]) # id changed
4522140752
>>> id(my_list[2]) # id still same as my_list[0], still referring to value `1`.
4522139440
Además de la respuesta aceptada que explica el problema correctamente, en lugar de crear una lista con elementos duplicados usando el siguiente código:
[[1]*4 for _ in range(3)]
Además, puedes utilizar itertools.repeat()
para crear un objeto iterador de elementos repetidos:
>>> a = list(repeat(1,4))
[1, 1, 1, 1]
>>> a[0] = 5
>>> a
[5, 1, 1, 1]
PD: Si estás usando NumPy y solo quieres crear una matriz de unos o ceros, puedes usar np.ones
y np.zeros
/o para otros números np.repeat
:
>>> import numpy as np
>>> np.ones(4)
array([1., 1., 1., 1.])
>>> np.ones((4, 2))
array([[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]])
>>> np.zeros((4, 2))
array([[0., 0.],
[0., 0.],
[0., 0.],
[0., 0.]])
>>> np.repeat([7], 10)
array([7, 7, 7, 7, 7, 7, 7, 7, 7, 7])