Mutabilidad, copias y matrices en Python — teoría con un carrito de la compra, zip y list comprehension (y la copia que no era una copia)
Quería tachar cosas de mi lista de la compra sin estropear la lista original, así que hice una «copia» para trabajar tranquilo: copia = carrito. Quité de la copia los huevos (ya los tenía en casa), imprimí las dos listas para comprobar que la original seguía intacta y esto fue lo que vi:
carrito = ["pan", "leche", "huevos"]
copia = carrito
copia.remove("huevos")
print("Copia:", copia)
print("Carrito:", carrito)
Copia: ['pan', 'leche'] Carrito: ['pan', 'leche']
«¿Cómo que los huevos han desaparecido de las dos? Yo solo he tocado la copia.» Revisé el remove, revisé el print, hasta dudé de mi ordenador. El problema no estaba en ninguna de esas líneas: <strong>copia = carrito</strong> no copia nada. Le pone un segundo nombre a la misma lista, y desde ese momento cualquier cambio se ve por los dos lados.
Hoy entendemos por qué pasa. Hablamos de mutabilidad (qué objetos pueden cambiar y cuáles no), de los métodos para modificar una lista, de las copias (superficiales y profundas), de cómo se comportan las listas dentro de las funciones, de enumerate y zip, de las list comprehension y de las matrices. Todo con un único ejemplo: el carrito de la compra. Si vienes de listas y tuplas en Python, este es el paso natural: allí aprendiste a leer y recorrer secuencias; aquí aprendes a cambiarlas sin que se te vuelvan en contra. Y cuando termines, la práctica te espera con tres programas completos.
Tabla de Contenidos
Mutable e inmutable: qué significa de verdad
Un objeto es mutable si puede cambiar de contenido sin dejar de ser el mismo objeto. Es inmutable si no puede: cualquier «cambio» en realidad crea un objeto nuevo y deja el anterior como estaba.
| Mutables | Inmutables |
|---|---|
list (lista) | int, float, bool (números y booleanos) |
dict, set (los veremos más adelante) | str (strings) |
tuple (tuplas) |
Es la diferencia que ya viste entre listas y tuplas, pero con consecuencias más profundas de las que parece. Un número inmutable no da problemas porque no hay forma de cambiarlo «por detrás». Una lista mutable sí: si dos nombres apuntan a la misma lista, los dos ven cualquier cambio. De eso va todo el artículo.
Modificar una lista
Una lista admite cambios en el sitio. Estas son las operaciones que más vas a usar, con el carrito como ejemplo:
carrito = ["pan", "leche", "huevos"]
carrito[0] = "pan integral" # asignar a una posición
carrito.append("arroz") # añadir al final
carrito.insert(1, "fruta") # insertar en una posición
carrito.extend(["sal", "aceite"]) # añadir varios al final
print(carrito)
carrito.remove("leche") # quitar por valor (el primero que aparezca)
ultimo = carrito.pop() # quitar y devolver el último
segundo = carrito.pop(1) # quitar y devolver el de la posición 1
del carrito[0] # quitar por posición, sin devolver nada
print(carrito, ultimo, segundo)
['pan integral', 'fruta', 'leche', 'huevos', 'arroz', 'sal', 'aceite'] ['huevos', 'arroz', 'sal'] aceite fruta
Entre líneas:
carrito[0] = "pan integral"sustituye lo que había en esa posición. Es la asignación por índice que con una tupla no podías hacer.append(x)añade un elemento al final;insert(pos, x)lo coloca en la posición que le digas y empuja al resto hacia la derecha;extend(otra)añade todos los elementos de otra secuencia. Si en lugar deextend(["sal", "aceite"])hubieras escritoappend(["sal", "aceite"]), habrías metido una lista dentro de la lista, como un único elemento.- Para quitar tienes tres opciones:
remove(valor)busca el valor y quita el primero que aparezca;pop()quita y devuelve el último (o el de la posición que le des, comopop(1)); ydel carrito[0]borra por posición sin devolver nada. - Fíjate en el orden de las líneas de la segunda tanda:
remove("leche")deja['pan integral', 'fruta', 'huevos', 'arroz', 'sal', 'aceite']; elpop()se llevaaceite; elpop(1)se llevafruta; ydel carrito[0]borrapan integral. Por eso al final quedan['huevos', 'arroz', 'sal'].
Usar remove con un valor que no está en la lista. A diferencia de in, que contesta True o False, remove protesta:
Traceback (most recent call last):
File "error_remove.py", line 2, in <module>
carrito.remove("huevos")
ValueError: list.remove(x): x not in list
Si no estás seguro de que el producto esté, pregunta antes: if "huevos" in carrito: carrito.remove("huevos").
Hay un detalle común a todos estos métodos que conviene grabarse ya: modifican la lista y devuelven None. No te devuelven la lista cambiada, porque la lista ya está cambiada. Más adelante verás por qué este detalle da tantos disgustos.
Ordenar: sorted frente a sort
Ordenar es el ejemplo perfecto de la diferencia entre «crear una lista nueva» y «modificar la que ya tengo». Python te da las dos versiones:
precios = [2.5, 0.8, 1.2, 3.0] ordenados = sorted(precios) # devuelve una lista nueva print(ordenados, precios) precios.sort() # ordena la propia lista print(precios) precios.reverse() # la da la vuelta print(precios) print(sorted(precios, reverse=True)) # de mayor a menor, sin tocar la original
[0.8, 1.2, 2.5, 3.0] [2.5, 0.8, 1.2, 3.0] [0.8, 1.2, 2.5, 3.0] [3.0, 2.5, 1.2, 0.8] [3.0, 2.5, 1.2, 0.8]
sorted(precios)es una función: devuelve una lista nueva ordenada y deja la original como estaba. Por eso en la primera línea se ven las dos listas distintas.precios.sort()es un método: ordena la propia lista, en el sitio. Y, como todos los métodos que modifican, no devuelve nada.reverse()hace lo mismo con el orden: da la vuelta a la lista, en el sitio. Si lo que quieres es ordenar de mayor a menor,sorted(..., reverse=True)(osort(reverse=True)) lo hace en un paso.
Y ahora, el error más famoso de todo el artículo:
None [0.8, 1.2, 2.5]
Escribir ordenados = precios.sort() esperando recibir la lista ordenada. sort() devuelve None, así que ordenados vale None; la lista ordenada es precios, que ha cambiado por dentro. Si quieres una variable nueva, usa sorted().
La regla de oro es que las funciones que devuelven (sorted) no tocan la original, y los métodos que modifican (sort, append, remove…) no devuelven nada. Casi nunca hacen las dos cosas a la vez.
Alias: dos nombres, una lista
Volvamos al misterio del principio. Cuando escribes b = a, Python no copia la lista: hace que b apunte al mismo objeto que a. Las variables en Python son etiquetas pegadas a objetos, no cajas con el contenido dentro. Dos etiquetas en el mismo objeto se llaman alias:
a = [1, 2, 3] b = a # b es OTRO NOMBRE para la misma lista b.append(4) print(a, b) print(a is b) # ¿son el mismo objeto?
[1, 2, 3, 4] [1, 2, 3, 4] True
Cambias la lista por b y la ves cambiada por a, porque solo hay una lista. El operador is lo confirma: a is b pregunta «¿son exactamente el mismo objeto?», y la respuesta es True.
Crear una «copia» con copia = lista. Es un alias, no una copia. Cualquier cambio a través de uno se ve en el otro, justo lo contrario de lo que querías.
Con los números y los strings esto nunca te ha dado problemas, y no es casualidad: como son inmutables, aunque dos nombres apunten al mismo objeto, nadie puede cambiarlo por dentro. Si haces x = 5; y = x; y = y + 1, el y + 1 crea un 6 nuevo y lo ata a y; el 5 que ve x sigue siendo 5. Con las listas, append o remove sí cambian el objeto, y eso es lo que se ve por todos los alias.
Copias: cómo tener una lista de verdad
Cuando quieres una lista independiente, tienes que pedirla. Hay tres formas equivalentes:
original = [1, 2, 3] c1 = original[:] # slice completo c2 = list(original) # constructor c3 = original.copy() # método c1.append(99) print(original, c1) print(original == c2, original is c2) # mismo contenido, distinto objeto print(original == c3, original is c3)
[1, 2, 3] [1, 2, 3, 99] True False True False
Entre líneas:
original[:]es un slice de principio a fin, y ya sabes que un slice crea una lista nueva.list(original)yoriginal.copy()hacen lo mismo con otra sintaxis. Usa la que te resulte más legible.- Al añadir
99ac1, la original no se entera: son dos listas distintas. ==compara el contenido yiscompara la identidad.c2yc3tienen el mismo contenido queoriginal(True), pero no son el mismo objeto (False). Con un alias, las dos preguntas daríanTrue.
La regla práctica es que == sirve para preguntar «¿tienen lo mismo?» y is para preguntar «¿es literalmente la misma lista?». Para comparar listas, casi siempre querrás ==.
Copia superficial y copia profunda
Las tres copias de arriba tienen una trampa. Son superficiales: crean una lista nueva, pero los elementos de dentro son los mismos objetos que en la original. Con números no se nota; con listas dentro de listas, sí:
import copy
tabla = [[1, 2], [3, 4]]
superficial = tabla.copy()
profunda = copy.deepcopy(tabla)
tabla[0][0] = 99
print("tabla:", tabla)
print("superficial:", superficial)
print("profunda:", profunda)
print(tabla[0] is superficial[0], tabla[0] is profunda[0])
tabla: [[99, 2], [3, 4]] superficial: [[99, 2], [3, 4]] profunda: [[1, 2], [3, 4]] True False
Mira la segunda línea: cambiamos tabla[0][0] y la copia superficial también cambió. La explicación está en la última línea: tabla[0] is superficial[0] es True. La copia superficial tiene una lista exterior nueva, pero sus filas son las mismas listas que las de la original. La copia profunda, con copy.deepcopy, duplica todo el árbol de objetos y por eso profunda no se entera de nada.
Copiar una lista de listas con [:] o .copy() y pensar que ya es independiente. Si vas a modificar elementos de dentro, necesitas copy.deepcopy o copiar cada fila a mano.
Mutabilidad y funciones
Todo lo anterior se nota especialmente al pasar una lista a una función. Cuando llamas a f(carrito), el parámetro es un alias de tu lista. Mira cuatro funciones que parecen parecidas y no lo son:
def quitar_ultimo(lista):
lista.pop() # modifica la lista original
def anadir_mal(lista, producto):
lista = lista + [producto] # crea una lista NUEVA y la ata al nombre local
def anadir_bien(lista, producto):
lista.append(producto) # modifica la lista original
def duplicar(numero, texto):
numero = numero * 2 # los números y los strings son inmutables
texto = texto + "!"
carrito = ["pan", "leche", "huevos"]
quitar_ultimo(carrito)
print(carrito)
anadir_mal(carrito, "arroz")
print(carrito)
anadir_bien(carrito, "arroz")
print(carrito)
n, t = 5, "hola"
duplicar(n, t)
print(n, t)
['pan', 'leche'] ['pan', 'leche'] ['pan', 'leche', 'arroz'] 5 hola
Entre líneas:
quitar_ultimousapop(), un método que modifica la lista. Como el parámetro y tucarritoson la misma lista, el cambio se ve fuera:huevosha desaparecido.anadir_malhacelista = lista + [producto]. Eso no modifica la lista:+crea una lista nueva, y la asignación solo ata el nombre locallistaa ella. Cuando la función termina, esa variable local desaparece y tucarritosigue como estaba. No hay error ni aviso: simplemente no pasa nada.anadir_bienhacelista.append(producto), que sí cambia el objeto compartido, y por esoarrozaparece.duplicardemuestra el caso inmutable:numero * 2ytexto + "!"crean objetos nuevos, así que fuera de la funciónnytsiguen valiendo5y"hola".
La regla es esta: asignar a un parámetro (lista = …) nunca cambia a quien llamó; modificar el objeto (lista.append(…)) sí.
Cuidado con +=. En una lista, lista += [producto] equivale a extend, así que modifica la original; en cambio lista = lista + [producto] crea una lista nueva. Se parecen mucho y se comportan distinto.
Una función que modifica sus argumentos puede sorprender a quien la llama. Una buena costumbre es que cada función haga una de dos cosas, y que lo diga su nombre y su docstring: o modifica (y no devuelve nada) o devuelve algo nuevo (y no toca los argumentos). Hablamos más de funciones en la práctica de funciones.
Una tupla que contiene una lista
Hay un caso que desconcierta al principio: una tupla es inmutable, pero ¿y si uno de sus elementos es una lista?
ficha = ("Lucía", [7.5, 8.0])
ficha[1].append(9.0) # la tupla no cambia: cambia la lista que contiene
print(ficha)
('Lucía', [7.5, 8.0, 9.0])
La tupla no ha cambiado: sigue guardando los mismos dos objetos (el string y la lista). Lo que ha cambiado es la lista, que es mutable. Lo que no puedes hacer es sustituir un elemento de la tupla por otro:
Traceback (most recent call last):
File "error_tupla_lista.py", line 2, in <module>
ficha[1] = []
~~~~~^^^
TypeError: 'tuple' object does not support item assignment
La inmutabilidad de una tupla es superficial: fija qué objetos contiene, no qué contenido tienen esos objetos.
Recorrer con enumerate y zip
En el artículo anterior recorrías por elemento (for producto in productos) o por índice (for pos in range(len(productos))). Python tiene dos funciones que cubren los casos intermedios con mucha más claridad:
productos = ["pan", "leche", "huevos"]
precios = [1.20, 0.95, 2.40]
for pos, producto in enumerate(productos):
print(pos, producto)
for pos, producto in enumerate(productos, 1): # empezar a contar en 1
print(f"{pos}. {producto}")
for producto, precio in zip(productos, precios): # recorrer dos listas a la vez
print(f"{producto}: {precio:.2f} €")
print(list(zip(productos, precios)))
0 pan
1 leche
2 huevos
1. pan
2. leche
3. huevos
pan: 1.20 €
leche: 0.95 €
huevos: 2.40 €
[('pan', 1.2), ('leche', 0.95), ('huevos', 2.4)]
enumerate(secuencia)va dando parejas(posición, elemento), yfor pos, producto in …las desempaqueta en dos variables. Es el reemplazo elegante derange(len(...))cuando necesitas el índice y el elemento a la vez.enumerate(productos, 1)empieza a contar en 1 en lugar de 0, ideal para numerar un ticket.zip(a, b)empareja elemento a elemento dos secuencias: el primero con el primero, el segundo con el segundo… Así recorres a la vezproductosypreciossin índices.zipdevuelve un objeto que se consume al recorrerlo; conlist(...)ves las parejas, que son tuplas.
Ahora, la trampa de zip:
productos = ["pan", "leche", "huevos", "arroz"]
precios = [1.20, 0.95, 2.40]
for producto, precio in zip(productos, precios):
print(producto, precio)
print(len(list(zip(productos, precios))))
pan 1.2 leche 0.95 huevos 2.4 3
Usar zip con dos listas de distinta longitud. zip para en cuanto se acaba la más corta, sin error ni aviso. Aquí hay cuatro productos y tres precios: el arroz se queda sin pareja y simplemente desaparece del recorrido.
Si tus dos listas tienen que medir lo mismo, compruébalo antes de empezar con assert len(productos) == len(precios). Un AssertionError a tiempo ahorra mucho tiempo de depuración.
List comprehension
Un patrón que repetirás sin parar es construir una lista nueva a partir de otra: aplicar algo a cada elemento, quedarte solo con algunos… Con un bucle son tres o cuatro líneas; con una list comprehension (lista por comprensión) es una:
precios = [1.20, 0.95, 2.40, 0.80, 3.10]
# con un bucle
con_iva = []
for precio in precios:
con_iva.append(round(precio * 1.21, 2))
# con list comprehension: lo mismo en una línea
con_iva2 = [round(precio * 1.21, 2) for precio in precios]
print(con_iva == con_iva2, con_iva2)
baratos = [p for p in precios if p < 1.5] # con condición
etiquetas = ["caro" if p >= 2 else "barato" for p in precios] # con if/else dentro
letras = [letra.upper() for letra in "pan"] # sobre cualquier secuencia
cuadrados = [n * n for n in range(1, 6)]
print(baratos)
print(etiquetas)
print(letras)
print(cuadrados)
True [1.45, 1.15, 2.9, 0.97, 3.75] [1.2, 0.95, 0.8] ['barato', 'barato', 'caro', 'barato', 'caro'] ['P', 'A', 'N'] [1, 4, 9, 16, 25]
La forma general es:
[expresión for elemento in secuencia if condición]
Se lee casi como una frase: «la expresión, para cada elemento de la secuencia, si cumple la condición». Entre líneas:
- La primera línea muestra que el bucle y la comprehension dan exactamente lo mismo (
True). Es una forma más corta de escribir el mismo recorrido con acumulador. - Con
ifal final filtras:baratossolo se queda con los precios menores de 1.5. - Con
if/elseal principio (antes delfor) eliges entre dos valores para cada elemento:etiquetasproduce una etiqueta por cada precio, así que tiene tantos elementos comoprecios. Fíjate en la diferencia: elifsolo filtra y va al final; elif/elsetransforma y va delante. - Funciona con cualquier secuencia, también un string (
letras) o unrange(cuadrados). - Una comprehension crea una lista nueva y no toca la original, igual que
sorted.
Si la comprehension se vuelve difícil de leer (varias condiciones, cálculos largos), escribe un bucle normal. Que quepa en una línea no la hace mejor; que se entienda, sí.
Para escribir una comprehension, primero escribe el bucle for normal. Después, saca la expresión que añadías con append y ponla delante del for.
Matrices: listas de listas
Una matriz es una tabla de valores con filas y columnas. En Python se representa como una lista de listas: cada elemento de la lista exterior es una fila. Aquí, las ventas de una tienda de tres productos durante tres días:
ventas = [
[12, 8, 15], # fila 0: lunes
[10, 7, 20], # fila 1: martes
[9, 11, 14], # fila 2: miércoles
]
print(ventas[1]) # una fila entera
print(ventas[1][2]) # fila 1, columna 2
print(len(ventas), len(ventas[0])) # filas y columnas
for fila in ventas: # recorrer por filas
print(fila, sum(fila))
for j in range(len(ventas[0])): # recorrer por columnas
columna = [fila[j] for fila in ventas]
print(j, columna, sum(columna))
ventas[0][1] = 80 # modificar un elemento
print(ventas[0])
[10, 7, 20] 20 3 3 [12, 8, 15] 35 [10, 7, 20] 37 [9, 11, 14] 34 0 [12, 10, 9] 31 1 [8, 7, 11] 26 2 [15, 20, 14] 49 [12, 80, 15]
Entre líneas:
ventas[1]es la fila 1 entera (una lista), yventas[1][2]es fila 1, columna 2: primero eliges la fila y después el elemento dentro de ella. Se lee de fuera hacia dentro.len(ventas)son las filas, ylen(ventas[0])las columnas (el largo de cualquier fila, si todas miden lo mismo).- Recorrer por filas es un
fornormal: cadafilaes una lista, y puedes sumarla directamente. - Recorrer por columnas es distinto: no existe «la columna 1» como objeto. Hay que recorrer los números de columna con
range(len(ventas[0]))y construir cada columna con una comprehension,[fila[j] for fila in ventas]. Este patrón (recorrer índices y sacar un elemento de cada fila) es la clave de casi todos los ejercicios de matrices. ventas[0][1] = 80cambia un solo elemento, igual que en cualquier lista.
Y ahora la trampa más clásica de las matrices, que mezcla todo lo que hemos visto en el artículo. Quieres un tablero de 3 por 3 lleno de ceros. [0] * 3 te da una fila de tres ceros, así que ¿por qué no repetir esa fila tres veces?
tablero = [[0] * 3] * 3 tablero[0][1] = 5 print(tablero) print(tablero[0] is tablero[1])
[[0, 5, 0], [0, 5, 0], [0, 5, 0]] True
Crear una matriz con [[0] * 3] * 3. El * 3 de fuera no crea tres filas distintas: repite la misma fila tres veces, es decir, tres alias de una sola lista. Por eso cambiar un elemento en una fila lo cambia en todas, y tablero[0] is tablero[1] es True.
La solución es construir cada fila por separado, y para eso está la comprehension:
tablero = [[0] * 3 for _ in range(3)] tablero[0][1] = 5 print(tablero) print(tablero[0] is tablero[1])
[[0, 5, 0], [0, 0, 0], [0, 0, 0]] False
Ahora, cada vuelta del bucle ejecuta [0] * 3 otra vez y crea una lista nueva, así que las tres filas son objetos distintos (False en el is). El _ es solo un nombre de variable que dice «no voy a usar este valor».
[0] * 3 sí es seguro cuando los elementos son inmutables (números, strings), porque nadie puede cambiarlos por dentro. El problema aparece cuando repites un objeto mutable, como una lista.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta tu programa línea a línea y dibuja las variables y los objetos en memoria: justo lo que hace falta para entender alias y copias. Elige Python 3 y pega este programa, que recoge el misterio del principio:
carrito = ["pan", "leche"]
alias = carrito
copia = carrito[:]
alias.append("huevos")
print(carrito)
print(copia)
print(carrito is alias, carrito is copia)
Salida esperada:
['pan', 'leche', 'huevos'] ['pan', 'leche'] True False
Qué observar, paso a paso:
- Tras la segunda línea, aparece una sola caja de lista con
"pan"y"leche", y dos flechas que apuntan a ella: la decarritoy la dealias. Dos nombres, un objeto. - En
copia = carrito[:]aparece una segunda caja con el mismo contenido, ycopiaapunta a ella. Mira que ahora hay dos listas distintas en memoria. - En
alias.append("huevos"), la primera caja crece. Comocarritoyaliasapuntan a ella, **los dos nombres venhuevos**; la caja decopiano cambia. - Los
printlo confirman:carritotiene los tres productos ycopiasolo dos. - La última línea saca
True False:carrito is aliasesTrue(mismo objeto) ycarrito is copiaesFalse(objetos distintos aunque empezaran con el mismo contenido). Es exactamente la diferencia entre un alias y una copia.
Resumen
| Concepto | Lo esencial |
|---|---|
| Mutable / inmutable | Las listas cambian «por dentro»; números, strings y tuplas, no |
| Modificar una lista | append, insert, extend, remove, pop, del: cambian la lista y devuelven None (salvo pop) |
sorted / sort | sorted(l) devuelve una lista nueva; l.sort() ordena en el sitio y devuelve None |
| Alias | b = a no copia: dos nombres para la misma lista |
| Copia superficial | l[:], list(l), l.copy(): lista nueva, pero con los mismos elementos |
| Copia profunda | copy.deepcopy(l): duplica también los objetos de dentro |
== / is | Mismo contenido / mismo objeto |
| Funciones | Modificar el objeto (append) se ve fuera; reasignar el parámetro (l = l + […]) no |
enumerate / zip | Posición y elemento a la vez / recorrer dos secuencias en paralelo (para en la más corta) |
| List comprehension | [expr for x in sec if cond]: una lista nueva en una línea |
| Matriz | Lista de listas: m[fila][columna]; por columnas, [f[j] for f in m] |
| Crear una matriz | [[0] * n for _ in range(m)], nunca [[0] * n] * m |
Tres ideas para llevarte:
- Una variable es una etiqueta, no una caja.
b = apega una segunda etiqueta al mismo objeto; si el objeto es mutable, los cambios se ven por las dos. - Hay dos formas de ordenar, copiar o filtrar: modificando o creando una nueva. Los métodos (
sort,append) modifican y devuelvenNone; las funciones y las comprehension (sorted,[… for …]) crean una lista nueva. Elegir bien evita la mitad de los errores de este tema. - Una matriz es una lista de listas, y cada fila tiene que ser su propia lista. Si repites una fila con
*, repites el mismo objeto; créalas con una comprehension y copia fila a fila.
En la práctica de mutabilidad, copias y matrices montarás un ticket de compra, un carrito que se modifica y una tabla de precios por tienda. Después, los ejercicios te dirán si ha quedado claro. Y en el siguiente tema entramos en una forma muy distinta de resolver problemas: la recursividad.
Siguiente paso: prácticaMutabilidad, copias y matrices en Python — práctica con un ticket de compra, un carrito que cambia y una tabla de precios (y la lista ordenada que era None)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

3 comentarios