Mutabilidad, copias y matrices en Python — ejercicios con una lista de la compra, un ticket y un stock de almacenes (y el producto que zip se comió)
Preparé el ticket de una compra de cuatro productos: pan, leche, huevos y arroz. Copié los precios de un papel, ejecuté el programa y me dio un total de 4.55. Me pareció poco, pero pensé «será una compra pequeña». No había ningún error, ni un aviso. Cuando por fin repasé las listas vi que tenía cuatro nombres y solo tres precios: se me había olvidado el del arroz. Y zip no me avisó de nada: simplemente dejó de recorrer cuando se acabó la lista más corta y el arroz se evaporó del ticket.
Los errores de este tema tienen todos el mismo aire: silenciosos, sin excepciones y con una salida que casi tiene sentido. Hoy practicamos con tres ejercicios de dificultad creciente, tres fragmentos para depurar y un chuletario para llevarte.
Estos ejercicios usan la teoría de mutabilidad, copias y matrices y repasan lo de la práctica. Puedes hacerlos sin ellas, pero te costará más.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado de un nivel y no mires la solución todavía.
- Escribe tu versión, ejecútala y compara tu salida con la esperada.
- Si te atascas, mira solo la 🔵 pista y vuelve a intentarlo.
- Cuando termines, compara con la solución y fíjate en por qué está hecha así, no solo en que dé la misma salida.
Ningún ejercicio usa input(). Los datos van fijos en el código para que tu salida sea idéntica a la esperada.
Nivel 1 · Básico: modificar una lista de la compra
Enunciado. Empieza con la lista compra = ["leche", "pan", "huevos"] y, modificándola en el sitio y en este orden: añade "arroz" al final, inserta "fruta" en la posición 1, quita el "pan", ordénala alfabéticamente y muéstrala. Después quita el último elemento guardándolo en una variable, muestra esa variable junto con la lista y, por último, muestra cuántos elementos quedan.
Salida esperada:
['arroz', 'fruta', 'huevos', 'leche'] leche ['arroz', 'fruta', 'huevos'] 3
Necesitas append, insert, remove y sort (todos modifican la lista, así que no guardes su resultado en ninguna variable). Para quitar el último y quedártelo, pop() hace las dos cosas. Antes de ordenar, la lista es ["leche", "fruta", "huevos", "arroz"].
Nivel 2 · Intermedio: el ticket de la compra
Enunciado. Con estos datos:
PRODUCTOS = ["Pan", "Leche", "Huevos", "Arroz"] PRECIOS = [1.20, 0.95, 2.40, 1.75] CANTIDADES = [2, 3, 1, 4]
escribe una función ticket(precios, cantidades) que devuelva una tupla (importes, total), donde importes es la lista con el precio por la cantidad de cada línea (calculada con zip y una list comprehension) y total es su suma. Después muestra cada producto con su importe a dos decimales, el total y una lista con los productos cuyo importe supera los 3 €.
Salida esperada:
Pan 2.40 Leche 2.85 Huevos 2.40 Arroz 7.00 Total: 14.65 Más de 3 €: ['Arroz']
zip(precios, cantidades) te da parejas (precio, cantidad), y la comprehension multiplica cada una. Para mostrar los productos junto a los importes, otro zip, esta vez de PRODUCTOS con importes. La lista de los caros es una comprehension con un if al final.
Nivel 3 · Desafío: el stock de tres almacenes
Enunciado. El stock de cuatro productos en tres almacenes es una matriz (una fila por almacén):
STOCK = [
[12, 5, 0, 8],
[3, 9, 14, 0],
[7, 0, 6, 10],
]
Escribe cuatro funciones: totales_por_almacen(stock) (la suma de cada fila), totales_por_producto(stock) (la suma de cada columna), transponer(stock) (la matriz con filas y columnas intercambiadas) y reponer(stock, almacen, cantidad), que devuelve un stock nuevo con cantidad unidades más de cada producto en ese almacén sin modificar el original. Muestra los tres primeros resultados, el almacén 1 repuesto con 5 unidades y el almacén 1 original para comprobar que no ha cambiado.
Salida esperada:
Por almacén: [25, 26, 23] Por producto: [22, 14, 20, 18] Transpuesta: [[12, 3, 7], [5, 9, 0], [0, 14, 6], [8, 0, 10]] Almacén 1 repuesto: [8, 14, 19, 5] Original: [3, 9, 14, 0]
Las sumas por filas son una comprehension con sum(fila). Para las columnas necesitas recorrer los índices de columna con range(len(stock[0])) y sacar fila[j] de cada fila; la transpuesta usa la misma idea, pero guardando la columna entera como fila. En reponer, empieza por copiar la matriz fila a fila.
¿Qué falla aquí?
Tres fragmentos con un fallo cada uno. Antes de leer la explicación, haz dos cosas: predice si da error y, si no lo da, qué imprime.
Fragmento 1
ticket.py
nombres = ["Pan", "Leche", "Huevos", "Arroz"]
precios = [1.20, 0.95, 2.40]
total = 0
for nombre, precio in zip(nombres, precios):
total += precio
print(total)
print(len(nombres), len(precios))
*Piénsalo antes de seguir: queremos sumar los precios de los cuatro productos. ¿Qué imprime python3 ticket.py?*
Explicación. No hay ningún error, pero la respuesta es incorrecta:
4.55 4 3
La primera línea es el total (¡debería ser 6.3!) y la segunda muestra el motivo: hay 4 nombres y solo 3 precios. zip para cuando se acaba la más corta, así que el arroz nunca llega al bucle.
Usar zip con listas de distinta longitud. No hay excepción: los elementos que sobran se descartan en silencio.
Arreglo. Completa el precio que faltaba y protege el programa con un assert que compruebe las longitudes antes de empezar:
nombres = ["Pan", "Leche", "Huevos", "Arroz"]
precios = [1.20, 0.95, 2.40, 1.75]
assert len(nombres) == len(precios), "faltan o sobran precios"
total = 0
for nombre, precio in zip(nombres, precios):
total += precio
print(round(total, 2))
print(len(nombres), len(precios))
6.3 4 4
Cuando dos listas deben ir «en paralelo», un assert len(a) == len(b) al principio convierte un fallo silencioso en un error con mensaje.
Fragmento 2
anadir.py
def anadir(producto, carrito=[]):
carrito.append(producto)
return carrito
lunes = anadir("pan")
martes = anadir("leche")
print(lunes)
print(martes)
¿Da error? ¿Qué imprime? Cada día debería empezar con un carrito vacío.
Explicación. Tampoco da error, y por eso es traicionero:
['pan', 'leche'] ['pan', 'leche']
El carrito del martes tiene el pan del lunes, y de hecho lunes y martes muestran lo mismo: son la misma lista. El valor por defecto carrito=[] se crea una sola vez, cuando Python lee la definición de la función, no en cada llamada. Todas las llamadas que no pasan carrito comparten esa misma lista, y cada append la va llenando.
Usar una lista (o cualquier objeto mutable) como valor por defecto de un parámetro. Es un alias escondido que sobrevive entre llamadas.
Arreglo. El valor por defecto es None, y la lista se crea dentro de la función, en cada llamada:
def anadir(producto, carrito=None):
if carrito is None:
carrito = []
carrito.append(producto)
return carrito
lunes = anadir("pan")
martes = anadir("leche")
print(lunes)
print(martes)
['pan'] ['leche']
La receta de siempre es parametro=None y, dentro, if parametro is None: parametro = [].
Fragmento 3
agotados.py
carrito = ["pan", "agotado", "agotado", "leche"]
for producto in carrito:
if producto == "agotado":
carrito.remove(producto)
print(carrito)
¿Da error? ¿Qué imprime? Quiero quitar todos los productos agotados.
Explicación. Se ejecuta sin errores, pero queda un «agotado» en el carrito:
['pan', 'agotado', 'leche']
Estás modificando la lista mientras la recorres. Al quitar el primer "agotado" (posición 1), el segundo se desplaza a la posición 1, pero el for ya va por la 2: lo salta sin mirarlo. Con dos elementos iguales seguidos, siempre se escapa uno.
Hacer remove o pop sobre una lista dentro de un for que la recorre. Los índices se mueven y el bucle se salta elementos.
Arreglo. En vez de quitar, construye una lista nueva solo con lo que quieres conservar:
carrito = ["pan", "agotado", "agotado", "leche"] carrito = [producto for producto in carrito if producto != "agotado"] print(carrito)
['pan', 'leche']
Una comprehension con if al final es la forma más corta de «quítame todos los que cumplan esto». Y como crea una lista nueva, no hay nada que se desplace.
Soluciones
Mira esta sección solo cuando hayas intentado el ejercicio. Si tu código es distinto pero da la misma salida y cumple los requisitos, también es una solución válida.
Solución del Nivel 1
compra.py
compra = ["leche", "pan", "huevos"]
compra.append("arroz")
compra.insert(1, "fruta")
compra.remove("pan")
compra.sort()
print(compra)
ultimo = compra.pop()
print(ultimo, compra)
print(len(compra))
Por qué está hecho así:
- Todos los métodos (
append,insert,remove,sort) modifican la lista en el sitio y devuelvenNone, por eso se llaman como instrucciones sueltas, sinx = …. Es justo el error de la lista que eraNone. - La secuencia paso a paso es:
["leche", "pan", "huevos"]→ conappendqueda[…, "arroz"]→ coninsert(1, "fruta")la fruta se cuela en la posición 1 →remove("pan")quita el primero que encuentra →sort()ordena alfabéticamente y deja['arroz', 'fruta', 'huevos', 'leche']. pop()hace dos cosas a la vez: quita el último (leche) y lo devuelve, y por eso lo guardamos enultimo. Es la excepción a «los métodos que modifican devuelvenNone«.len(compra)cuenta lo que queda tras elpop: tres productos.
Solución del Nivel 2
ticket.py
def ticket(precios, cantidades):
"""Devuelve (importes, total) con un importe por línea."""
importes = [precio * cantidad for precio, cantidad in zip(precios, cantidades)]
return importes, sum(importes)
PRODUCTOS = ["Pan", "Leche", "Huevos", "Arroz"]
PRECIOS = [1.20, 0.95, 2.40, 1.75]
CANTIDADES = [2, 3, 1, 4]
importes, total = ticket(PRECIOS, CANTIDADES)
for producto, importe in zip(PRODUCTOS, importes):
print(producto, f"{importe:.2f}")
print("Total:", f"{total:.2f}")
caros = [producto for producto, importe in zip(PRODUCTOS, importes) if importe > 3]
print("Más de 3 €:", caros)
Por qué está hecho así:
- La función devuelve
(importes, total)y no modifica sus argumentos: crea la lista de importes nueva con una comprehension. Se puede llamar con los mismos datos las veces que haga falta. zip(precios, cantidades)empareja el precio y la cantidad de cada línea en la misma posición. Conprecio * cantidadobtenemos el importe de cada una.return importes, sum(importes)empaqueta dos valores en una tupla sin paréntesis, y la llamada los desempaqueta enimportes, total.- El
forposterior vuelve a usarzip, esta vez para recorrer a la vezPRODUCTOSeimportes, sin índices. El f-string{importe:.2f}da dos decimales: así7.0se ve como7.00. caroses una comprehension con filtro: elifva al final y solo deja pasar los importes mayores que 3. Solo el arroz (4 × 1.75 = 7.00) lo cumple; la leche se queda en 2.85.- Las cuentas cuadran: 2.40 + 2.85 + 2.40 + 7.00 = 14.65.
Solución del Nivel 3
almacenes.py
STOCK = [
[12, 5, 0, 8], # almacén 0
[3, 9, 14, 0], # almacén 1
[7, 0, 6, 10], # almacén 2
]
def totales_por_almacen(stock):
return [sum(fila) for fila in stock]
def totales_por_producto(stock):
return [sum([fila[j] for fila in stock]) for j in range(len(stock[0]))]
def transponer(stock):
return [[fila[j] for fila in stock] for j in range(len(stock[0]))]
def reponer(stock, almacen, cantidad):
"""Devuelve un stock NUEVO con 'cantidad' unidades más de cada producto en ese almacén."""
nuevo = [fila[:] for fila in stock]
nuevo[almacen] = [unidades + cantidad for unidades in nuevo[almacen]]
return nuevo
print("Por almacén:", totales_por_almacen(STOCK))
print("Por producto:", totales_por_producto(STOCK))
print("Transpuesta:", transponer(STOCK))
repuesto = reponer(STOCK, 1, 5)
print("Almacén 1 repuesto:", repuesto[1])
print("Original:", STOCK[1])
Por qué está hecho así:
totales_por_almacenrecorre por filas: cadafilaes una lista ysumla suma entera. Una comprehension y listo.totales_por_productorecorre por columnas: no existe «la columna» como objeto, así que se recorren los índicesjy se construye cada columna con[fila[j] for fila in stock], que se suma. Fíjate en que el rango sale delen(stock[0]), el número de columnas.transponerusa exactamente la misma idea, pero en lugar de sumar guarda cada columna como una fila nueva: una comprehension dentro de otra. Las 3 filas de 4 elementos se convierten en 4 filas de 3.reponerno modificastock: empieza connuevo = [fila[:] for fila in stock], que copia cada fila, y reemplaza la fila del almacén por otra lista nueva con las unidades sumadas. Aquí, como reemplazamos la fila entera, bastaría con copiar la lista exterior; copiamos las filas igualmente para que la función siga siendo segura el día que la cambies para modificar elementos sueltos.- La última línea de la salida lo demuestra: el almacén 1 repuesto tiene
[8, 14, 19, 5], ySTOCK[1]sigue siendo[3, 9, 14, 0]. - Las cuentas cuadran: la columna 0 suma 12 + 3 + 7 = 22, y la fila 1 repuesta es 3 + 5, 9 + 5, 14 + 5 y 0 + 5.
Míralo paso a paso en Python Tutor
El fallo del fragmento 3 se ve mejor en vivo. Python Tutor ejecuta el código línea a línea y dibuja las listas y sus cambios. Elige Python 3 y pega este programa, que quita los agotados de dos maneras:
carrito = ["pan", "agotado", "agotado", "leche"]
for producto in carrito:
if producto == "agotado":
carrito.remove(producto)
print(carrito)
otro = ["pan", "agotado", "agotado", "leche"]
limpio = [p for p in otro if p != "agotado"]
print(limpio, otro)
Salida esperada:
['pan', 'agotado', 'leche'] ['pan', 'leche'] ['pan', 'agotado', 'agotado', 'leche']
Qué observar, paso a paso:
- En el primer
for,carritoes una lista de cuatro elementos yproductorecorre las posiciones 0, 1, 2 y 3 de la lista que existe en cada momento. En la primera vuelta,productovale"pan"y no pasa nada. - En la segunda vuelta,
productovale"agotado"(posición 1) y elifse cumple. Tras elremove, la lista encoge: ahora tiene tres elementos y el segundo"agotado"ha pasado a la posición 1. - La tercera vuelta mira la posición 2, que ahora es
"leche". El segundo"agotado"está ahora en la posición 1, que el bucle ya dejó atrás, y nunca se examina. - El primer
printmuestra la lista con un"agotado"que no se ha ido. Sin ninguna excepción: el bucle simplemente se saltó un elemento. - En la segunda parte,
otrono cambia nunca (compruébalo en el dibujo) ylimpioes una lista nueva con solo"pan"y"leche". Construir en lugar de modificar evita el problema.
Chuletario: Mutabilidad, copias y matrices
Lo esencial en 30 segundos
- Mutable:
list(tambiéndictyset). Inmutable: números,strytuple. - **
b = ano copia:** crea un alias. Un cambio porbse ve ena. - Copia superficial:
l[:],list(l)ol.copy(). Para listas de listas,copy.deepcopy(l)o copiar cada fila. - **Los métodos que modifican devuelven
None** (sort,append,remove…);popes la excepción, porque devuelve lo que quita. - Ordenar sin tocar la original:
sorted(l). Ordenar en el sitio:l.sort(). - **
zippara en la lista más corta.** Si deben medir lo mismo, comprueba las longitudes. - Matriz nueva:
[[0] * n for _ in range(m)], nunca[[0] * n] * m.
Sintaxis
| Quiero… | Código | Resultado |
|---|---|---|
| Añadir al final | l.append(x) | Modifica l; devuelve None |
| Insertar en una posición | l.insert(pos, x) | Empuja el resto a la derecha |
| Añadir varios | l.extend(otra) | Cada elemento de otra |
| Quitar por valor | l.remove(x) | El primero; ValueError si no está |
| Quitar y quedarme con él | l.pop() / l.pop(pos) | Devuelve el elemento |
| Ordenar sin tocar la original | sorted(l, reverse=True) | Lista nueva |
| Ordenar la propia lista | l.sort() | Modifica; devuelve None |
| Copiar | l[:] / list(l) / l.copy() | Lista nueva (superficial) |
| Copiar en profundidad | copy.deepcopy(l) | Todo el árbol duplicado |
| Mismo contenido / mismo objeto | a == b / a is b | True o False |
| Numerar | for pos, x in enumerate(l, 1): | Parejas (pos, x) desde 1 |
| Recorrer dos listas | for a, b in zip(l1, l2): | Para en la más corta |
| Transformar o filtrar | [f(x) for x in l if cond] | Lista nueva |
| Elemento de una matriz | m[i][j] | Fila i, columna j |
| Una columna | [f[j] for f in m] | Lista con la columna j |
| Crear una matriz | [[0] * n for _ in range(m)] | m filas independientes |
Plantillas de los ejercicios
**Ticket con zip y comprehension** (Nivel 2):
importes = [precio * cantidad for precio, cantidad in zip(precios, cantidades)] total = sum(importes) caros = [p for p, i in zip(productos, importes) if i > 3]
Totales por filas y por columnas (Nivel 3):
por_fila = [sum(fila) for fila in m] por_columna = [sum([fila[j] for fila in m]) for j in range(len(m[0]))]
Transponer una matriz (Nivel 3):
transpuesta = [[fila[j] for fila in m] for j in range(len(m[0]))]
Copiar una matriz sin alias (Nivel 3):
copia = [fila[:] for fila in m]
Parámetro con valor por defecto seguro (Fragmento 2):
def anadir(producto, carrito=None):
if carrito is None:
carrito = []
carrito.append(producto)
return carrito
Errores que más se repiten
| Síntoma | Causa probable |
|---|---|
| La «copia» cambia cuando cambia la original | Es un alias (copia = lista): usa l[:] o l.copy() |
Una variable vale None tras ordenar o añadir | x = l.sort() o x = l.append(…): esos métodos devuelven None |
ValueError: list.remove(x): x not in list | remove de un valor ausente: comprueba antes con in |
| Cambias una fila de la matriz y cambian todas | [[0] * n] * m repite la misma fila: crea las filas con una comprehension |
| La copia de una matriz cambia con la original | Copia superficial: copia cada fila o usa copy.deepcopy |
| El ticket tiene menos líneas de las esperadas | zip con listas de distinta longitud: compara los len |
| Una función «recuerda» datos de llamadas anteriores | Parámetro por defecto mutable (carrito=[]): usa None |
Quedan elementos sin quitar tras un for con remove | Modificas la lista mientras la recorres: usa una comprehension |
| La función no cambia mi lista | Reasignas el parámetro (l = l + […]) en lugar de modificar el objeto |
TypeError: 'tuple' object does not support item assignment | Intentas cambiar un elemento de una tupla: es inmutable |
Antes de entregar, pregúntate
- ¿Estoy modificando la lista o creando una nueva? ¿Lo hace evidente mi función (nombre y docstring)?
- Si copio algo, ¿de verdad es una copia, y llega hasta el nivel que voy a modificar (filas incluidas)?
- Si uso
zip, ¿miden lo mismo mis listas? - ¿He guardado en una variable el resultado de un método que devuelve
None(sort,append,remove…)?
Si quieres repasar el tema desde la base, vuelve a la teoría de mutabilidad, copias y matrices.
Sigue aprendiendoTodos los artículos de Python¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
