Tiza Paso a paso
Practicar →

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.

Practicar este tema →8 ejercicios que te corrigen paso a paso · gratis, sin cuenta

La explicación

La sorpresa

a = [1, 2, 3]
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é

Una variable no contiene una lista: apunta a ella. 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.copy()     # copia
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

def agregar_cero(lista):
    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.

Esto no es un defecto, es una herramienta — pero hay que hacerlo a propósito. Si no querés tocar la original, copiá adentro de la función:
def agregar_cero(lista):
    nueva = lista.copy()
    nueva.append(0)
    return nueva

Modificar contra reasignar

b.append(4)   → modifica la lista compartida: a también cambia
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 == b   → ¿tienen el mismo contenido?
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:

def registrar(x, acumulada=[]):
    acumulada.append(x)
    return acumulada

registrar(1)  → [1]
registrar(2)  → [1, 2]     # ¡se acordó del anterior!
registrar(3)  → [1, 2, 3]
Las tres llamadas escriben sobre la misma lista, la que quedó guardada en la definición. La forma correcta usa None como marca de «no me pasaron nada» y crea la lista adentro:
def registrar(x, acumulada=None):
    if acumulada is None:
        acumulada = []
    acumulada.append(x)
    return acumulada
Ahora las tres llamadas dan [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.
Un ejercicio resuelto, paso a paso

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

a = [1, 2, 3]   → se crea una lista, y a apunta a ella
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:

b = a.copy()

Ahora b.append(4) deja a en [1, 2, 3].

Ojo con una variante que confunde: si en vez de 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.

El mismo control, sin mirar contenidos. 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.

Y uno del nivel del parcial

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:

[1, 2, 3] [1, 2, 3] True

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:

a = [1, 2]             → se crea una lista; a apunta a ella
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:

def agregar_sin_tocar(lista, x):
    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:

a queda [1, 2]   b es [1, 2, 3]   a is b → False

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.

def agregar2(x, lista=[]):
    lista.append(x)
    return lista

Lo que imprime:

agregar2(1)  → [1]
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:

def agregar3(x, lista=None):
    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

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

Los otros temas de la unidad 4

Estructuras de datos

Ver todos los temas de Pensamiento Computacional