Pensamiento Computacional · Unidad 4 — Estructuras de datos
Mutabilidad y referencias
Con listas, b = a no copia nada: los dos nombres apuntan a la misma lista. Tocás una y cambia la otra.
La explicación
La sorpresa
b = a
b.append(4)
print(a) → [1, 2, 3, 4]
Tocamos b y cambió a. Con números esto no pasaba: en el tema 2.1,
después de y = x podías cambiar x todo lo que quisieras y
y ni se enteraba.
Por qué
b = a no crea una
lista nueva, sólo agrega otro nombre para la misma. Como si una persona tuviera apodo:
llamala como la llames, es la misma persona.Con números no se nota porque son inmutables: no se pueden modificar en el lugar. Cuando hacés
x = x + 1 no cambiás el 5, creás un 6 y apuntás ahí.
Cómo copiar de verdad
b = a[:] # también (rebanada de todo)
b = list(a) # también
Cualquiera de las tres crea una lista nueva. A partir de ahí son independientes.
Lo mismo pasa con las funciones
lista.append(0)
mis_numeros = [1, 2]
agregar_cero(mis_numeros)
print(mis_numeros) → [1, 2, 0]
La función no devuelve nada, pero igual modificó la lista de afuera, porque recibió una referencia a la misma lista. Con un número esto no pasaría.
nueva = lista.copy()
nueva.append(0)
return nueva
Modificar contra reasignar
b = [9, 9] → apunta b a otra lista: a queda igual
La diferencia es si tocás la lista o si movés el nombre. Es la distinción clave del tema y la que más se pregunta.
Cómo preguntar si son la misma
a is b → ¿son el mismo objeto?
Con b = a las dos preguntas dan True. Con c = a.copy(), en cambio, a == c da True y a is c da False: mismo contenido, dos listas distintas. Ese is es la forma rápida de verificar en la consola si una copia se copió de verdad.
El parámetro por defecto que se acuerda de todo
Una trampa que parece un detalle y no lo es. El valor por defecto de un parámetro se crea una sola vez, cuando se define la función, no cada vez que se la llama:
acumulada.append(x)
return acumulada
registrar(1) → [1]
registrar(2) → [1, 2] # ¡se acordó del anterior!
registrar(3) → [1, 2, 3]
None como marca de «no me pasaron nada» y crea la lista adentro:
if acumulada is None:
acumulada = []
acumulada.append(x)
return acumulada
[1], [2] y [3]. La regla para
llevarse: nunca pongas una lista o un diccionario como valor por defecto. Con números y
textos no pasa, justamente porque son inmutables.
¿Por qué este programa imprime [1, 2, 3, 4] y no [1, 2, 3]? a = [1, 2, 3] b = a b.append(4) print(a)
Paso a paso:
b = a → b apunta a la misma, no se crea ninguna nueva
b.append(4) → se modifica esa única lista
print(a) → a apunta a la lista modificada: [1, 2, 3, 4]
El punto clave está en la línea 2. b = a no copia la lista: copia
la dirección, y quedan dos nombres para el mismo objeto. Con números pasa lo mismo, pero no se nota, porque un número no se puede modificar en el lugar.
Si querías que fueran independientes:
Ahora b.append(4) deja a en [1, 2, 3].
b.append(4) escribieras
b = b + [4], a quedaría intacta aunque no hayas copiado. Es que
b + [4] crea una lista nueva y le mueve el nombre a b; nunca
se toca la original. Modificar y reasignar no son lo mismo.
Control: ante la duda, imprimí las dos variables después de cada línea. Si cambian juntas, comparten lista.
print(a is b) contesta la pregunta
de una: True significa que son la misma lista y que lo que le hagas a una le pasa
a la otra. Después de b = a da True; después de
c = a.copy() da False aunque a == c siga dando
True. Es más directo que comparar impresiones, porque no depende de que las
listas tengan valores distintos para notar la diferencia.
Y el mismo fenómeno adentro de una función. Si le pasás la lista a una función que hace
lista.append(9), la de afuera queda modificada — el parámetro es otra etiqueta
sobre la misma lista. Si la función hace lista = lista + [9], la de afuera no
cambia, porque eso arma una lista nueva y le mueve el nombre local. Es la misma regla de
siempre, y cuando una función «no hace nada» o «hace de más» sobre una lista, es esto.
Explicar qué imprime este programa y por qué: def agregar(lista, x): lista.append(x); return lista · a = [1, 2] · b = agregar(a, 3) · print(a, b, a is b). Mostrar después el problema del parámetro por defecto mutable.
Lo que imprime:
Las tres cosas sorprenden por la misma razón: en Python las variables no guardan objetos, guardan referencias a objetos. a y lista y b son tres nombres para la misma lista.
Qué pasó, paso a paso:
agregar(a, 3) → el parámetro "lista" apunta a la MISMA
lista.append(x) → modifica esa lista, no una copia
b = ... → b apunta también a la misma
a is b pregunta si son el mismo objeto, no si tienen el mismo contenido. Para el contenido está ==. Acá is da True porque hay una sola lista con tres nombres.
La versión que no toca el original:
return lista + [x]
La diferencia es append contra +. append modifica la lista existente; lista + [x] construye una lista nueva y deja la original como estaba. Con esta versión:
Cuál de las dos está "bien" depende del enunciado. Si la función se llama agregar, modificar tiene sentido; si se llama con_un_elemento_mas, no. Lo que no está bien es no saber cuál estás usando.
Y ahora la trampa que este mismo mecanismo esconde: el parámetro por defecto mutable.
lista.append(x)
return lista
Lo que imprime:
agregar2(2) → [1, 2] (no [2])
agregar2(3) → [1, 2, 3]
Por qué. El valor por defecto [] se evalúa una sola vez, cuando Python define la función, no cada vez que se la llama. Así que todas las llamadas que no pasan una lista comparten la misma, y cada append se acumula sobre la anterior.
Es el mismo fenómeno del principio: hay un solo objeto lista, y varios lugares lo referencian. Solo que acá el que lo referencia es la definición de la función, y eso lo hace difícil de ver.
La forma correcta, que es la que hay que usar siempre:
if lista is None:
lista = []
lista.append(x)
return lista
Con None como valor por defecto, la lista nueva se crea en cada llamada, adentro del cuerpo de la función. Ahí sí: agregar3(1) da [1] y agregar3(2) da [2].
La regla: nunca poner una lista, un diccionario o un conjunto como valor por defecto de un parámetro. Los números, los strings y los booleanos no tienen este problema porque no se pueden modificar.
Dónde se pierde el punto: contestar que a vale [1, 2] "porque la función recibió una copia". Python no copia los argumentos: pasa la referencia, y con objetos mutables eso se nota.
Dónde se cae la mayoría
- Creer que b = a copia la lista. No copia: son dos nombres para la misma.
- Modificar una lista adentro de una función sin darse cuenta de que cambia la de afuera.
- Confundir modificar (append, sort) con reasignar (b = [...]): la primera se ve afuera, la segunda no.
Hasta acá la explicación. La práctica es la otra mitad: 8 ejercicios de este tema, en cuatro niveles, que no te dicen sólo si está bien al final sino que te corrigen en cada paso y te explican por qué. Y uno para escribir el programa vos, que se prueba ejecutándolo en Python ahí mismo, en el navegador. Además trae una animación que muestra la idea en movimiento.
Practicar Mutabilidad y referencias →Dónde entra este tema en el parcial
- Primer parcial — Trazar: copiar una lista de listas10 puntos · resuelto paso a paso
Los otros temas de la unidad 4
Estructuras de datos