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:

  1. Datos que no deben cambiar (coordenadas, fechas, registros). Garantía de inmutabilidad.
  2. Más rápidas y compactas que listas (menos overhead).
  3. Pueden ser claves de diccionario (las listas no, porque son mutables).
  4. 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:

  1. El catálogo vive como lista de tuplas — fácil agregar/cambiar productos.
  2. El programa puede aceptar muchos productos por pedido (cap 6 de PRO115 solo aceptaba uno por compra).
  3. 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.

✏️ 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.

✏️ Ejercicio 1.3 — Tabla de multiplicar

Generá una matriz 10×1010\times10 de tabla de multiplicar como lista de listas, usando comprehension. Imprimila formateada.

✏️ Ejercicio 1.4 — Mediana sin numpy

Escribí función mediana(lista) sin usar bibliotecas externas.

✏️ 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"))

1.14 Para profundizar


Definiciones nuevas: lista, tupla, índice, slicing, mutabilidad, alias, list comprehension, lambda, map, filter, reduce, enumerate, zip.