Lista de cambios de listas reflejados en sublistas inesperadamente

Resuelto Charles Anderson asked hace 15 años • 19 respuestas

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

Charles Anderson avatar Oct 27 '08 21:10 Charles Anderson
Aceptado

Cuando escribes, [x]*3obtienes, 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]*4cada 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] * 4texto 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] * 4cada vez por la misma razón [x**2 for x in range(3)]reevalúa x**2cada vez. Cada evaluación de [1] * 4genera una nueva lista, por lo que la comprensión de la lista hace lo que usted desea.

Por cierto, [1] * 4tampoco copia los elementos de [1], pero eso no importa, ya que los números enteros son inmutables. No puedes hacer algo como 1.value = 2convertir un 1 en un 2.

CAdaker avatar Oct 27 '2008 15:10 CAdaker
size = 3
matrix_surprise = [[0] * size] * size
matrix = [[0]*size for _ in range(size)]

Visualización en vivo usando Python Tutor:

Marcos y objetos

nadrimajstor avatar Aug 26 '2013 23:08 nadrimajstor

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

PierreBdR avatar Oct 27 '2008 15:10 PierreBdR
[[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.

Blair Conrad avatar Oct 27 '2008 15:10 Blair Conrad

my_list = [[1]*4] * 3crea 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 objreflejará en tres lugares, dondequiera que objse 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 1es inmutable, obj = [1]*4seguirá creando una lista 1repetida 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
jerrymouse avatar Apr 06 '2017 05:04 jerrymouse

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.onesy 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])
Mazdak avatar Jun 17 '2015 17:06 Mazdak