Mutabilidad, 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)
Quería saber cuál era el producto más caro y el más barato de mi carrito, así que ordené los precios. Escribí ordenados = precios.sort(), imprimí ordenados para ver el resultado y la pantalla me contestó con una sola palabra: None. Ni la lista, ni un error, ni un aviso. Una lista que había estado ahí hacía un segundo se había convertido en «nada».
La lista no se había perdido: sort() la había ordenado por dentro, y lo único que devolvía era None, que yo estaba guardando muy contento en ordenados. Es el tipo de fallo que se arregla quitando media línea y que cuesta un rato largo encontrar si no sabes que los métodos que modifican una lista no devuelven nada.
Hoy practicamos mutabilidad, copias, zip, enumerate, list comprehension y matrices con tres programas completos: un ticket de la compra, un carrito que se modifica en el sitio y una tabla de precios por tienda. Esta práctica se apoya en la teoría de mutabilidad, copias y matrices. Si todavía dudas con las listas básicas, repasa antes listas y tuplas en Python.
Tabla de Contenidos
Cómo usar este artículo
Cada programa va completo en un solo archivo. Cópialo, guárdalo con el nombre que se indica y lánzalo desde la terminal con python3 nombre.py. Antes de mirar la salida, intenta predecirla: es la mejor forma de saber si has entendido el código.
Ningún programa usa input(). Los datos van fijos en el código para que tu salida sea idéntica a la que ves aquí.
Programa 1 · El ticket de la compra
Tenemos la compra en tres listas paralelas: los productos, sus precios y las cantidades, siempre en el mismo orden. Queremos un ticket numerado con el importe de cada línea, el subtotal, el IVA y el total, y una lista con las líneas que superan los 5 €. Es el programa perfecto para zip (recorrer listas en paralelo), enumerate (numerar) y una comprehension.
ticket.py
PRODUCTOS = ["Pan", "Leche", "Huevos", "Arroz", "Aceite"]
PRECIOS = [1.20, 0.95, 2.40, 1.75, 6.90]
CANTIDADES = [2, 3, 1, 2, 1]
IVA = 0.10
def importes(precios, cantidades):
"""Importe de cada línea: precio por cantidad."""
return [precio * cantidad for precio, cantidad in zip(precios, cantidades)]
lineas = importes(PRECIOS, CANTIDADES)
for num, (producto, cantidad, importe) in enumerate(zip(PRODUCTOS, CANTIDADES, lineas), 1):
print(f"{num}. {producto} x{cantidad} -> {importe:.2f} €")
subtotal = sum(lineas)
print(f"Subtotal: {subtotal:.2f} €")
print(f"IVA ({IVA:.0%}): {subtotal * IVA:.2f} €")
print(f"Total: {subtotal * (1 + IVA):.2f} €")
caras = [producto for producto, importe in zip(PRODUCTOS, lineas) if importe > 5]
print("Líneas de más de 5 €:", caras)
Lánzalo con python3 ticket.py:
1. Pan x2 -> 2.40 € 2. Leche x3 -> 2.85 € 3. Huevos x1 -> 2.40 € 4. Arroz x2 -> 3.50 € 5. Aceite x1 -> 6.90 € Subtotal: 18.05 € IVA (10%): 1.81 € Total: 19.86 € Líneas de más de 5 €: ['Aceite']
Entre líneas:
importesconstruye una lista nueva con una comprehension: para cada pareja(precio, cantidad)que le dazip, calculaprecio * cantidad. No modifica nada y devuelve la lista de importes.- El
forprincipal es el más denso del programa:zip(PRODUCTOS, CANTIDADES, lineas)empareja las tres listas a la vez (zipadmite todas las que quieras) yenumerate(…, 1)numera cada tripleta empezando en 1. Cada vuelta recibe algo como(1, ("Pan", 2, 2.4)), y el patrónnum, (producto, cantidad, importe)lo desempaqueta de golpe, incluida la tupla de dentro. {importe:.2f}fuerza dos decimales, y{IVA:.0%}convierte el0.10en10%.- Las cuentas cuadran: 2.40 + 2.85 + 2.40 + 3.50 + 6.90 = 18.05. El IVA es 1.805, que se muestra como 1.81, y 18.05 + 1.81 = 19.86, que es el total.
carases una comprehension con filtro: solo se queda con los productos cuyo importe pasa de 5 €. Solo el aceite lo cumple (6.90).- Este programa no modifica ninguna lista: crea listas nuevas a partir de las anteriores. Es la manera segura de trabajar cuando no necesitas cambiar los datos de origen.
Pruébalo tú
- Quita el último precio de
PRECIOS(deja cuatro) y ejecuta. ¿Qué pasa con el ticket? ¿Dónde está el aceite? ¿Por qué no hay ningún error? - Escribe una comprehension que genere una lista con los importes de
lineascon un 15 % de descuento, y otra que se quede solo con los productos cuya cantidad sea mayor que 1.
Programa 2 · Un carrito que cambia
Ahora el carrito es una lista de tuplas (producto, precio) que vamos a modificar con funciones. Aquí se ve la diferencia entre las funciones que cambian el carrito en el sitio y las que devuelven uno nuevo, justo lo que la teoría llama mutabilidad.
carrito.py
def anadir(carrito, producto, precio):
"""Añade un producto al final del carrito (modifica el carrito)."""
carrito.append((producto, precio))
def quitar(carrito, producto):
"""Quita el primer producto con ese nombre. Devuelve True si lo quitó."""
for pos, (nombre, precio) in enumerate(carrito):
if nombre == producto:
carrito.pop(pos)
return True
return False
def con_descuento(carrito, porcentaje):
"""Devuelve un carrito NUEVO con los precios rebajados (no toca el original)."""
return [(nombre, round(precio * (1 - porcentaje), 2)) for nombre, precio in carrito]
def total(carrito):
"""Suma de los precios del carrito."""
return sum([precio for nombre, precio in carrito])
carrito = [("Pan", 1.20), ("Leche", 0.95)]
anadir(carrito, "Arroz", 1.75)
anadir(carrito, "Aceite", 6.90)
print(carrito)
print("Quitar Leche:", quitar(carrito, "Leche"))
print("Quitar Café:", quitar(carrito, "Café"))
print(carrito)
rebajado = con_descuento(carrito, 0.20)
print(rebajado)
print(carrito)
print(f"Total: {total(carrito):.2f} € -> {total(rebajado):.2f} €")
precios = [precio for nombre, precio in carrito]
ordenados = sorted(precios, reverse=True)
print("Precios de mayor a menor:", ordenados)
print("Lista original:", precios)
Lánzalo con python3 carrito.py:
[('Pan', 1.2), ('Leche', 0.95), ('Arroz', 1.75), ('Aceite', 6.9)]
Quitar Leche: True
Quitar Café: False
[('Pan', 1.2), ('Arroz', 1.75), ('Aceite', 6.9)]
[('Pan', 0.96), ('Arroz', 1.4), ('Aceite', 5.52)]
[('Pan', 1.2), ('Arroz', 1.75), ('Aceite', 6.9)]
Total: 9.85 € -> 7.88 €
Precios de mayor a menor: [6.9, 1.75, 1.2]
Lista original: [1.2, 1.75, 6.9]
Entre líneas:
anadiryquitarmodifican el carrito que reciben (appendypop). Por esoanadirno devuelve nada y se llama sin guardar el resultado: el cambio ya se ve fuera. La primera línea de salida lo demuestra: tras dos llamadas, el carrito tiene cuatro productos.quitarrecorre conenumeratepara conocer la posición, y en cuanto encuentra el producto hacepop(pos)yreturn Trueinmediatamente. Parar justo después de quitar no es un capricho: seguir recorriendo una lista a la que acabas de quitar un elemento haría que las posiciones se corrieran. Si no lo encuentra, llega al final del bucle y devuelveFalse.- Por eso
quitar(carrito, "Leche")daTruey la leche desaparece, yquitar(carrito, "Café")daFalsey el carrito se queda igual. con_descuentohace lo contrario: no toca el carrito original y devuelve uno nuevo, construido con una comprehension. Cada elemento es una tupla nueva(nombre, precio rebajado), porque las tuplas no se pueden modificar. La prueba está en la salida:rebajadotiene los precios con el 20 % de descuento y elprint(carrito)siguiente sigue mostrando los precios de antes.totalsuma los precios con una comprehension que desempaqueta cada tupla. Los dos totales (9.85 y 7.88) cuadran con las listas de arriba.- Las dos últimas líneas son la historia del principio:
sorted(precios, reverse=True)devuelve una lista nueva ordenada de mayor a menor, ypreciossigue en su orden original. Si hubiéramos escritoprecios.sort(reverse=True), la lista original habría cambiado y no habría nada que guardar.
Pruébalo tú
- Escribe
ordenados = precios.sort(reverse=True)e imprimeordenadosyprecios. ¿Qué sale en cada uno y por qué? - Cambia
con_descuentopara que modifique el carrito en el sitio, recorriendo por índice y asignandocarrito[pos] = (nombre, nuevo_precio). ¿Qué cambia en la salida? ¿Qué función dejaría de tener sentido devolver nada?
Programa 3 · Una tabla de precios por tienda
Para terminar, una matriz: tres tiendas (filas) y tres productos (columnas). Queremos el coste de una unidad de cada producto en cada tienda, la tienda más barata para cada producto, una tabla nueva con descuento y una copia de la matriz que podamos modificar sin romper la original.
tiendas.py
TIENDAS = ["Mercado", "Súper", "Online"]
PRODUCTOS = ["Pan", "Leche", "Aceite"]
PRECIOS = [
[1.20, 0.85, 6.90], # Mercado
[1.10, 0.95, 7.20], # Súper
[1.35, 0.80, 6.50], # Online
]
# Una unidad de cada producto, por tienda (la suma de cada fila)
for tienda, fila in zip(TIENDAS, PRECIOS):
print(tienda, round(sum(fila), 2))
# Producto a producto: la tienda más barata (recorrido por columnas)
for j, producto in enumerate(PRODUCTOS):
columna = [fila[j] for fila in PRECIOS]
barata = columna.index(min(columna))
print(producto, "->", TIENDAS[barata], min(columna))
# Una tabla nueva con un 10 % de descuento (comprehension dentro de otra)
rebajados = [[round(precio * 0.90, 2) for precio in fila] for fila in PRECIOS]
print(rebajados[0])
print(PRECIOS[0])
# Copiar una matriz: hay que copiar cada fila
copia = [fila[:] for fila in PRECIOS]
copia[1][0] = 0.99
print(PRECIOS[1], copia[1])
Lánzalo con python3 tiendas.py:
Mercado 8.95 Súper 9.25 Online 8.65 Pan -> Súper 1.1 Leche -> Online 0.8 Aceite -> Online 6.5 [1.08, 0.77, 6.21] [1.2, 0.85, 6.9] [1.1, 0.95, 7.2] [0.99, 0.95, 7.2]
Entre líneas:
- El primer bucle recorre por filas:
zip(TIENDAS, PRECIOS)empareja cada tienda con su lista de precios, ysum(fila)suma una fila entera.round(…, 2)evita que aparezcan decimales raros de la aritmética de coma flotante. Cuadra: 1.20 + 0.85 + 6.90 = 8.95 en el mercado. - El segundo bucle recorre por columnas. Con
enumerate(PRODUCTOS)tenemos la posiciónjde cada producto, y[fila[j] for fila in PRECIOS]saca esa columna. Después,columna.index(min(columna))da la posición del mínimo, que es también la posición de la tienda: el pan es más barato en el súper (1.10) y la leche y el aceite, en la tienda online. rebajadoses una comprehension dentro de otra: la de fuera crea una fila por cada fila, y la de dentro rebaja cada precio. El resultado es una matriz nueva; la primera fila de los precios originales se imprime después sin cambios ([1.2, 0.85, 6.9]).- Al final, la copia:
[fila[:] for fila in PRECIOS]copia cada fila. Si hubiéramos escritocopia = PRECIOS[:](o.copy()), tendríamos una lista exterior nueva pero con las mismas filas, es decir, una copia superficial: modificarcopia[1][0]habría cambiado tambiénPRECIOS. Así,copia[1]tiene el 0.99 yPRECIOS[1]conserva su 1.1.
Pruébalo tú
- Cambia
[fila[:] for fila in PRECIOS]porPRECIOS[:]y vuelve a ejecutar. ¿Qué sale ahora en la última línea? Compruébalo conPRECIOS[1] is copia[1]. - Añade una función
tienda_mas_barata(precios)que devuelva el nombre de la tienda cuya fila suma menos. Pista: una comprehension consum(fila)te da los totales ymin+indexte dan la posición.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta tu código línea a línea y dibuja variables y objetos en memoria. Elige Python 3 y pega este programa, que compara las dos formas de crear una matriz de 2 por 2 que vimos en la tabla de precios:
mal = [[0] * 2] * 2 bien = [[0] * 2 for _ in range(2)] mal[0][0] = 5 bien[0][0] = 5 print(mal) print(bien) print(mal[0] is mal[1], bien[0] is bien[1])
Salida esperada:
[[5, 0], [5, 0]] [[5, 0], [0, 0]] True False
Qué observar, paso a paso:
- Tras las dos primeras líneas,
males una lista exterior con dos flechas que apuntan a la misma lista interior, mientras quebientiene dos flechas hacia dos listas interiores distintas. La diferencia de dibujo es toda la diferencia del error. - Al ejecutar
mal[0][0] = 5, sigue la flecha de la fila 0 hasta su lista y cambia la posición 0. Como la fila 1 apunta a esa misma lista, el 5 aparece en las dos filas. - Con
bien[0][0] = 5pasa lo esperado: solo cambia la primera fila, porque la segunda es una lista independiente. - Los dos primeros
printlo confirman:[[5, 0], [5, 0]]frente a[[5, 0], [0, 0]]. - La última línea saca
True False: enmal, las dos filas son el mismo objeto; enbien, no. Python Tutor te lo enseña con las flechas, peroiste lo dice por escrito.
Para terminar
Hoy has visto en acción las herramientas de este tema: zip y enumerate para recorrer sin índices a mano, comprehension para construir listas nuevas, funciones que modifican y funciones que devuelven, y matrices recorridas por filas y por columnas, con su copia correcta. Si algo no te ha cuadrado, vuelve a la teoría; y cuando te sientas cómodo, pasa a los ejercicios, donde los fallos vienen incluidos.
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Un comentario