Listas y tuplas
"Las variables guardan UN dato. Las listas guardan MUCHOS. Esa diferencia es la frontera entre programas escolares y programas útiles."
Qué vas a aprender en este capítulo
En Programación I tus programas manejaban un dato a la vez: una nota, un precio, una edad. La realidad rara vez es así — manejás 50 notas, 100 productos, miles de clientes. En este capítulo aprendés a agrupar datos en listas y tuplas, a indexarlos, recorrerlos, modificarlos y construir programas que escalen. También vas a conocer la list comprehension, una herramienta tan poderosa que cuando la tenés, no querés escribir un for clásico nunca más.
1.1 La idea: una caja con muchos compartimentos
💡 Intuición
En Programación I:
nota1 = 7
nota2 = 8
nota3 = 6
Si fueran 30 notas, ¿declarás nota1 hasta nota30 a mano? No, esa no es la solución. La solución se llama lista:
notas = [7, 8, 6, 9, 10, 5, 8, ...]
Una lista es una sola variable que guarda muchos valores ordenados. Le pedís el primero con notas[0], el segundo con notas[1], y así.
Una lista es como una cajonera con muchos compartimentos numerados. Cada uno guarda un valor. Pedís uno por su número (índice).
Las listas hacen posible:
- Iterar sobre todos los datos con un solo
for. - Buscar, contar, ordenar sin escribir código repetido.
- Crecer y achicar dinámicamente.
Sin listas, programar sería sufrimiento. Con listas, los programas reales se vuelven posibles.
1.2 Crear listas
📐 Fundamento
Sintaxis básica:
vacia = []
numeros = [1, 2, 3, 4, 5]
mezclada = [1, "hola", 3.14, True] # Python permite tipos mezclados
anidada = [[1, 2], [3, 4], [5, 6]] # lista de listas
Notar:
- Corchetes
[ ]. - Elementos separados por coma.
- Pueden contener cualquier cosa (incluso otras listas).
- En Python, mezclar tipos es legal pero no recomendado — confunde al lector.
Crear con range:
numeros = list(range(10)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
pares = list(range(0, 20, 2)) # [0, 2, 4, ..., 18]
Repetir:
ceros = [0] * 10 # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
ab = ["a", "b"] * 3 # ['a', 'b', 'a', 'b', 'a', 'b']
Importante con la repetición: funciona bien con valores inmutables. Con mutables (listas, dicts) hay un trampa que veremos después.
1.3 Acceso por índice
📐 Fundamento
Indexado. Empieza en 0.
notas = [7, 8, 6, 9, 10]
notas[0] # 7 (primer elemento)
notas[3] # 9 (cuarto)
notas[-1] # 10 (último — índice negativo cuenta desde el final)
notas[-2] # 9 (anteúltimo)
notas[10] # IndexError: list index out of range
Por qué desde 0: convención heredada de C. Los primeros lenguajes (Fortran) empezaban en 1, pero la mayoría moderna usa 0. Acostumbrate, no vale pelearse.
Modificar:
notas[0] = 8
print(notas) # [8, 8, 6, 9, 10]
Eso solo funciona si la posición existe. Para agregar nuevos al final:
notas.append(7) # agrega al final
notas.insert(0, 5) # inserta en posición 0
notas.extend([3, 4]) # agrega varios al final
Borrar:
del notas[0] # borra por índice
notas.remove(8) # borra el primer 8 (por valor)
ultimo = notas.pop() # quita y devuelve el último
primero = notas.pop(0) # quita y devuelve el de posición 0
Tamaño:
len(notas)
1.4 Slicing (rebanadas)
📐 Fundamento
Una de las features más amadas de Python: extraer un trozo de una lista.
Sintaxis: lista[inicio:fin:paso]
inicio(incluido),fin(excluido).- Si omitís alguno, default: inicio del array, fin del array, paso 1.
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
a[2:5] # [2, 3, 4]
a[:3] # [0, 1, 2] (desde el inicio)
a[5:] # [5, 6, 7, 8, 9] (hasta el final)
a[:] # copia completa
a[::2] # [0, 2, 4, 6, 8] (cada 2)
a[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] (al revés)
a[-3:] # [7, 8, 9] (los últimos 3)
Slicing también modifica:
a[2:5] = [99, 99]
print(a) # [0, 1, 99, 99, 5, 6, 7, 8, 9] (reemplazó posiciones 2-4)
Slicing en cadenas (que son secuencias también):
nombre = "Maria"
nombre[1:3] # "ar"
nombre[::-1] # "airaM" (reverso clásico)
Truco famoso: lista[::-1] es la forma idiomática de invertir.
1.5 Operaciones útiles
📐 Fundamento
| Operación | Qué hace |
|---|---|
len(L) |
Cantidad de elementos |
L.append(x) |
Agrega al final |
L.insert(i, x) |
Inserta en posición |
L.pop(), L.pop(i) |
Quita y devuelve |
L.remove(x) |
Quita primera ocurrencia de x |
L.clear() |
Vacía |
L.sort() |
Ordena IN-PLACE |
sorted(L) |
Devuelve copia ordenada |
L.reverse() |
Invierte IN-PLACE |
reversed(L) |
Devuelve un iterador inverso |
L.index(x) |
Posición de la primera x |
L.count(x) |
Cuántas veces aparece x |
x in L |
True si x está |
min(L), max(L), sum(L) |
Estadísticas básicas |
L1 + L2 |
Concatena |
Diferencia importante: in-place vs new.
a = [3, 1, 2]
a.sort() # modifica a → [1, 2, 3]
b = sorted(a) # devuelve nueva, a no cambia
Convención en Python: las funciones que modifican devuelven None. Si esperabas el resultado, te confundís:
b = a.sort() # ¡b es None!
sorted y reversed (sin método) devuelven copia. Métodos .sort() y .reverse() modifican.
Ordenar con criterio personalizado:
gente = ["Maria", "ana", "JOSE", "carlos"]
gente.sort() # alfabético: 'JOSE','Maria','ana','carlos' (Unicode)
gente.sort(key=str.lower) # alfabético sensato
gente.sort(key=len) # por longitud
gente.sort(key=len, reverse=True) # invertido
key recibe una función que aplica a cada elemento para extraer la "clave de ordenamiento".
1.6 Recorrer listas
📐 Fundamento
Forma idiomática (mejor):
for nota in notas:
print(nota)
for le pide a la lista uno por uno. Más limpio que for i in range(len(...)).
Cuando necesitás el índice:
for i, nota in enumerate(notas):
print(f"Nota {i}: {nota}")
Recorrer dos listas en paralelo:
nombres = ["Ana", "Beto", "Carla"]
notas = [7, 8, 6]
for n, nt in zip(nombres, notas):
print(f"{n}: {nt}")
zip empareja elementos en posición. Si las listas son de distinto largo, se detiene en la corta.
Buscar un elemento (forma estructurada):
def buscar(lista, x):
for i, elem in enumerate(lista):
if elem == x:
return i
return -1 # no encontrado
(Esto ya existe como lista.index(x), pero el ejercicio mental vale.)
1.7 Listas son MUTABLES — la trampa
⚠️ Trampa común
Las listas son mutables: pueden cambiar después de crearse. Eso es bueno, pero introduce sutilezas peligrosas.
Trampa 1: alias
a = [1, 2, 3]
b = a # ¿copia? ¡NO!
b.append(4)
print(a) # [1, 2, 3, 4] ← a también cambió!
b = a no copia — hace que ambos nombres apunten al mismo objeto. Una modificación se ve en ambos.
Para copiar:
b = a.copy() # método
b = a[:] # slicing
b = list(a) # constructor
import copy
b = copy.deepcopy(a) # copia profunda (necesaria si hay listas anidadas)
Trampa 2: argumento mutable por defecto
def agregar(x, lista=[]): # ¡NO!
lista.append(x)
return lista
agregar(1) # [1]
agregar(2) # [1, 2] ← acumulado!
La lista del default se crea una sola vez al definir la función. Cada llamada modifica la misma. Solución idiomática:
def agregar(x, lista=None):
if lista is None:
lista = []
lista.append(x)
return lista
Trampa 3: multiplicar lista anidada
matriz = [[0] * 3] * 3 # ¡NO!
matriz[0][0] = 1
print(matriz) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
Las tres "filas" son la misma lista. Modificar una afecta todas.
Correcto:
matriz = [[0] * 3 for _ in range(3)]
(Esa expresión es una list comprehension, próxima sección.)
1.8 Tuplas — listas inmutables
📐 Fundamento
Una tupla es como una lista, pero no se puede modificar después de crearse.
Sintaxis:
punto = (3, 4)
sin_parentesis = 3, 4 # también es tupla
una_sola = (5,) # ¡coma obligatoria!
vacia = ()
Operaciones: las mismas que listas excepto las que modifican.
punto[0] # 3 — lectura sí
len(punto) # 2
punto[0] = 99 # ❌ TypeError: 'tuple' object does not support item assignment
Por qué existen las tuplas:
- Datos que no deben cambiar (coordenadas, fechas, registros). Garantía de inmutabilidad.
- Más rápidas y compactas que listas (menos overhead).
- Pueden ser claves de diccionario (las listas no, porque son mutables).
- Múltiples valores de retorno de funciones — pattern muy usado.
def divmod_propio(a, b):
return a // b, a % b # devuelve tupla
q, r = divmod_propio(17, 5) # desempaca
Desempacado:
x, y = (3, 4) # x=3, y=4
a, b, c = [1, 2, 3] # también funciona con listas
# Intercambio elegante:
a, b = b, a # sin variable temporal
Tupla con * para "el resto":
primero, *resto = [1, 2, 3, 4, 5]
print(primero) # 1
print(resto) # [2, 3, 4, 5]
Cuándo lista, cuándo tupla:
| Lista | Tupla |
|---|---|
| Cuando los datos son del mismo tipo y se pueden agregar/quitar | Cuando los datos son heterogéneos y fijos |
| Carrito de compras | Coordenada |
| Lista de tareas | Fecha (año, mes, día) |
| Cola de espera | Versión (1, 2, 3) |
1.9 List comprehensions
📐 Fundamento
La feature más pythónica de Python. Permite construir una lista a partir de otra en una sola línea legible.
Sintaxis básica:
[expresion for elemento in iterable]
Ejemplos:
# Cuadrados de 1 a 10
cuadrados = [x ** 2 for x in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# Mayúsculas
nombres = ["ana", "beto", "carla"]
mayus = [n.upper() for n in nombres]
# ["ANA", "BETO", "CARLA"]
# Equivalente con bucle (peor):
mayus = []
for n in nombres:
mayus.append(n.upper())
Con filtro:
[expresion for elem in iterable if condicion]
# Pares al cuadrado
[x ** 2 for x in range(20) if x % 2 == 0]
# [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]
Anidadas (matrices):
matriz = [[i * j for j in range(5)] for i in range(5)]
Con condicional ternario en la expresión:
# Etiquetar como par/impar
etiquetas = ["par" if x % 2 == 0 else "impar" for x in range(10)]
Cuándo NO usar comprehension. Si la lógica es compleja (varios if/else anidados, side-effects), un for clásico es más legible. La regla es: una línea legible es mejor que tres clásicas; tres líneas crípticas son peor que ocho clásicas.
1.10 Operaciones funcionales: map, filter, reduce
📐 Fundamento
Aunque Python prefiere comprehensions, conviene conocer las primitivas funcionales — son más comunes en otros lenguajes y siguen apareciendo.
map(función, iterable) — aplica función a cada elemento.
nombres = ["ana", "beto"]
mayus = list(map(str.upper, nombres)) # ["ANA", "BETO"]
# Equivale a:
mayus = [n.upper() for n in nombres]
filter(función, iterable) — deja solo los que cumplen.
nums = [1, 2, 3, 4, 5, 6]
pares = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4, 6]
# Equivale a:
pares = [x for x in nums if x % 2 == 0]
reduce(función, iterable) — acumula. Está en functools.
from functools import reduce
nums = [1, 2, 3, 4, 5]
total = reduce(lambda a, b: a + b, nums) # 15
# Equivale a:
total = sum(nums)
Lambdas. Funciones anónimas inline:
cuadrado = lambda x: x ** 2
print(cuadrado(5)) # 25
Útiles cuando una función es chiquita y se usa solo una vez (en map, filter, sort key=).
En la práctica: Python prefiere comprehensions. map y filter aparecen en código de otros lenguajes (Java streams, JavaScript, Lisp). Conocelos pero no los uses obsesivamente en Python.
1.11 Proyecto: catálogo de pupusería
🏗️ Avance del proyecto
Avanzamos el sistema de la pupusería. Ahora el catálogo es una lista:
# pupuseria_v1.py - catálogo en listas
# Cada producto es una tupla (nombre, precio)
catalogo = [
("Pupusa de queso", 0.50),
("Pupusa revuelta", 0.60),
("Pupusa de chicharron", 0.60),
("Pupusa de frijol", 0.55),
("Curtido (extra)", 0.25),
("Refresco tamarindo", 1.00),
]
IVA = 0.13
def imprimir_menu(catalogo):
print("\n=== Menú ===")
for i, (nombre, precio) in enumerate(catalogo, start=1):
print(f" {i}. {nombre:30} ${precio:.2f}")
print()
def pedir_pedido(catalogo):
"""Devuelve lista de tuplas (índice, cantidad)."""
pedido = []
while True:
s = input("Producto # (Enter para terminar): ").strip()
if not s:
break
if not s.isdigit():
print(" Número inválido")
continue
i = int(s) - 1
if i < 0 or i >= len(catalogo):
print(" No existe")
continue
cant_s = input(" Cantidad: ").strip()
if not cant_s.isdigit() or int(cant_s) <= 0:
print(" Cantidad inválida")
continue
pedido.append((i, int(cant_s)))
return pedido
def calcular_recibo(pedido, catalogo):
"""Devuelve lista de líneas y subtotal."""
lineas = []
subtotal = 0
for idx, cant in pedido:
nombre, precio = catalogo[idx]
importe = cant * precio
lineas.append((cant, nombre, importe))
subtotal += importe
return lineas, subtotal
def imprimir_recibo(lineas, subtotal):
print("\n=== RECIBO ===")
for cant, nombre, importe in lineas:
print(f" {cant}× {nombre:30} ${importe:>6.2f}")
iva = subtotal * IVA
total = subtotal + iva
print("-" * 50)
print(f" Subtotal{'':32} ${subtotal:>6.2f}")
print(f" IVA (13%){'':31} ${iva:>6.2f}")
print(f" TOTAL{'':35} ${total:>6.2f}")
def main():
imprimir_menu(catalogo)
pedido = pedir_pedido(catalogo)
if not pedido:
print("Sin pedido. Bye.")
return
lineas, subtotal = calcular_recibo(pedido, catalogo)
imprimir_recibo(lineas, subtotal)
if __name__ == "__main__":
main()
Mejoras logradas:
- El catálogo vive como lista de tuplas — fácil agregar/cambiar productos.
- El programa puede aceptar muchos productos por pedido (cap 6 de PRO115 solo aceptaba uno por compra).
- Cada función hace una cosa, es testeable.
Próximo capítulo: convertir el catálogo a diccionario para búsqueda más rápida y agregar inventario.
1.12 Resumen visual
| Concepto | Una línea |
|---|---|
Lista [ ] |
Secuencia mutable, indexada desde 0 |
Tupla ( ) |
Secuencia inmutable |
len, in |
Tamaño, pertenencia |
append, insert, pop, remove |
Modificar |
sort() vs sorted() |
In-place vs nueva |
Slicing a[i:j:k] |
Trozo |
enumerate, zip |
Iterar con índice / paralelo |
[expr for x in iter] |
List comprehension |
map, filter, reduce |
Funcional, casi reemplazado por comprehensions |
| Mutabilidad | Listas sí, tuplas no — cuidado con alias |
1.13 Ejercicios
✏️ Ejercicio 1.1 — Operaciones básicas
Dada la lista notas = [5, 8, 3, 9, 7, 6, 10, 4], escribí código que:
a. Imprima la mayor. b. Imprima el promedio. c. Imprima cuántas son ≥ 7. d. Imprima la lista ordenada de mayor a menor.
Solución
notas = [5, 8, 3, 9, 7, 6, 10, 4]
print("Mayor:", max(notas))
print("Promedio:", sum(notas) / len(notas))
print("Aprobadas:", sum(1 for n in notas if n >= 7))
print("Desc:", sorted(notas, reverse=True))
sum(1 for ...) es un truco para contar: cada match suma 1.
✏️ Ejercicio 1.2 — Eliminar duplicados manteniendo orden
Escribí función unicos(lista) que devuelva una nueva lista sin duplicados, manteniendo el orden de aparición.
Solución
def unicos(lista):
visto = set()
resultado = []
for x in lista:
if x not in visto:
visto.add(x)
resultado.append(x)
return resultado
print(unicos([1, 3, 2, 1, 4, 3, 5])) # [1, 3, 2, 4, 5]
Versión más pythónica con dict (ordenados desde Python 3.7):
def unicos(lista):
return list(dict.fromkeys(lista))
dict.fromkeys crea un dict con las claves dadas, y los dicts mantienen orden de inserción ignorando duplicados.
✏️ Ejercicio 1.3 — Tabla de multiplicar
Generá una matriz de tabla de multiplicar como lista de listas, usando comprehension. Imprimila formateada.
Solución
tabla = [[i * j for j in range(1, 11)] for i in range(1, 11)]
for fila in tabla:
for n in fila:
print(f"{n:4}", end="")
print()
Salida:
1 2 3 ...
2 4 6 ...
3 6 9 ...
...
✏️ Ejercicio 1.4 — Mediana sin numpy
Escribí función mediana(lista) sin usar bibliotecas externas.
Solución
def mediana(lista):
s = sorted(lista)
n = len(s)
if n == 0:
raise ValueError("lista vacía")
if n % 2 == 1:
return s[n // 2]
return (s[n // 2 - 1] + s[n // 2]) / 2
print(mediana([3, 1, 4, 1, 5, 9, 2, 6])) # (3+4)/2 = 3.5
✏️ Ejercicio 1.5 — Detección de bug
¿Por qué este código está mal? ¿Qué imprime?
def agregar(item, carrito=[]):
carrito.append(item)
return carrito
print(agregar("pupusa"))
print(agregar("refresco"))
print(agregar("curtido"))
Solución
Imprime:
['pupusa']
['pupusa', 'refresco']
['pupusa', 'refresco', 'curtido']
Bug: el default [] se evalúa una vez al definir la función. Cada llamada modifica el mismo objeto.
Arreglo:
def agregar(item, carrito=None):
if carrito is None:
carrito = []
carrito.append(item)
return carrito
Es uno de los gotchas más conocidos. Si lo entendés, ya sos un programador Python intermedio.
1.14 Para profundizar
- Documentación oficial: https://docs.python.org/es/3/tutorial/datastructures.html
- Fluent Python (Luciano Ramalho), capítulo "Una matriz de secuencias".
- Próximo capítulo: Diccionarios y conjuntos — mapeos clave→valor y conjuntos sin duplicados.
Definiciones nuevas: lista, tupla, índice, slicing, mutabilidad, alias, list comprehension, lambda, map, filter, reduce, enumerate, zip.