Bucles for sobre iterables: el modelo correcto

El for de Python no es el for de C ni el de Java. No cuenta desde cero hasta n-1. Lo que hace es pedir el siguiente elemento a cualquier objeto que sepa dárselo, uno por uno, hasta que se acabe. Eso es todo. Y esa diferencia de diseño cambia cómo escribes prácticamente todo el código que usa bucles.

El mecanismo interno se llama protocolo iterador: cuando Python ejecuta for x in cosa, llama a iter(cosa) para obtener un iterador, y luego llama a next() repetidamente sobre ese iterador hasta que lanza StopIteration. Cualquier objeto que implemente __iter__ (o __getitem__) participa en este protocolo —listas, tuplas, strings, diccionarios, archivos, generadores, rangos, lo que sea. No necesitas saber cuántos elementos hay, ni acceder por posición.

Esto explica por qué for i in range(len(lista)): es casi siempre un antipatrón: estás generando índices artificiales para después usarlos como lista[i], cuando Python ya puede darte el elemento directamente. Salvo que necesites el índice por alguna razón real, escribir así solo añade ruido y abre la puerta a errores de off-by-one.

¿Cuándo sí necesitas el índice? Cuando lo usas para algo más que acceder al elemento —escribir en otra lista en la misma posición, comparar con el anterior, etc. Para eso existe enumerate(), que te da índice y valor juntos sin la indirección manual. Y cuando iteras sobre dos colecciones en paralelo, zip() las une elemento a elemento sin que tengas que gestionar ningún contador.

Lo que se rompe si ignoras todo esto: código más largo, menos legible, y vulnerable a bugs sutiles. Si cambias una lista por un generador (que no tiene len()), el patrón range(len(...)) revienta directamente.

from pathlib import Path


def demo_iteracion():
    # ── 1. El patrón base: iterar directamente sobre la colección ──────────
    frutas = ["manzana", "pera", "kiwi"]

    for fruta in frutas:
        print(fruta)

    # ── 2. enumerate(): índice + valor sin range(len(...)) ─────────────────
    for i, fruta in enumerate(frutas):
        print(f"{i}: {fruta}")

    # enumerate acepta start= para arrancar desde otro número
    for i, fruta in enumerate(frutas, start=1):
        print(f"#{i} → {fruta}")

    # ── 3. zip(): dos (o más) iterables en paralelo ────────────────────────
    precios = [1.20, 0.80, 2.50]

    for fruta, precio in zip(frutas, precios):
        print(f"{fruta}: {precio}€")

    # zip trunca al más corto sin aviso; si eso es un bug, usa strict=True
    # (disponible desde Python 3.10)
    try:
        for fruta, precio in zip(frutas, [1.20, 0.80], strict=True):
            print(fruta, precio)
    except ValueError as e:
        print(f"Longitudes diferentes: {e}")

    # ── 4. for sobre un archivo: el fichero es un iterable de líneas ───────
    ruta = Path("notas.txt")
    ruta.write_text("línea uno\nlínea dos\nlínea tres\n", encoding="utf-8")

    with open(ruta, encoding="utf-8") as f:
        for linea in f:                   # no hace falta readlines() ni índices
            print(linea.rstrip("\n"))

    ruta.unlink()

    # ── 5. for sobre un generador: funciona exactamente igual ─────────────
    def cuadrados(n):
        for x in range(n):
            yield x ** 2                  # produce un valor cada vez que se pide

    for valor in cuadrados(5):
        print(valor)


demo_iteracion()

Desglose del código

La primera sección no tiene sorpresas, pero es el punto de partida: for fruta in frutas es el patrón universal. Python llama a iter(frutas), y cada iteración llama a next() internamente. No hay índice, no hay contador explícito.

enumerate(frutas) devuelve un iterador de tuplas (índice, valor). El desempaquetado for i, fruta in ... extrae ambos en una sola línea. El parámetro start=1 es especialmente útil para listas numeradas en output humano, sin que tengas que hacer i + 1 dentro del cuerpo.

zip(frutas, precios) construye un iterador de tuplas ("manzana", 1.20), ("pera", 0.80), etc. La regla de truncado —al iterable más corto— es el comportamiento por defecto porque es el más común y más seguro. Pero si tus dos listas deberían tener siempre la misma longitud y no la tienen, eso es un bug silencioso. zip(strict=True) lo convierte en un ValueError inmediato, que es exactamente lo que quieres.

El bucle sobre el archivo demuestra que for linea in f ya usa el protocolo iterador del objeto fichero, que lee una línea por petición sin cargar todo en memoria. Es la forma idiomática y eficiente de procesar archivos grandes.

El generador cuadrados produce valores bajo demanda con yield. Para el for, es indistinguible de una lista: llama a next() en cada iteración y para cuando llega StopIteration. Puedes reemplazar la lista por un generador y el bucle no cambia una sola línea.

Errores que debes conocer

Error: usar range(len(lista)) cuando solo necesitas los elementos, lo que introduce un índice innecesario y rompe con iterables sin longitud.

# ❌ Wrong
nombres = ["Ana", "Luis", "Marta"]
for i in range(len(nombres)):
    print(nombres[i])

# ✅ Right
for nombre in nombres:
    print(nombre)

El for directo funciona con cualquier iterable, es más corto y deja claro que el índice no importa.


Error: usar zip() para combinar listas que deberían tener la misma longitud sin detectar discrepancias, ocultando un bug de datos.

# ❌ Wrong  — la lista corta trunca sin aviso
ids   = [1, 2, 3]
names = ["Alice", "Bob"]          # se perdió el tercero en silencio
for id_, name in zip(ids, names):
    print(id_, name)

# ✅ Right
for id_, name in zip(ids, names, strict=True):   # Python 3.10+
    print(id_, name)

strict=True lanza ValueError si las longitudes no coinciden, convirtiendo un bug silencioso en un error detectable inmediatamente.


Error: llamar a next() manualmente sobre un iterador y luego pasarlo a for, consumiendo el primer elemento sin querer.

# ❌ Wrong
it = iter([10, 20, 30])
primero = next(it)               # consume el 10
for x in it:                     # itera solo sobre 20 y 30
    print(x)

# ✅ Right
datos = [10, 20, 30]
primero = datos[0]               # accede por índice si necesitas el primero
for x in datos:                  # itera sobre toda la lista
    print(x)

Un iterador tiene estado: avanza y no vuelve. Si mezclas next() manual con for sobre el mismo iterador, pierdes elementos.

50

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio