`while`: iteración cuando no sabes cuántos ciclos necesitas

En Python, for itera sobre algo concreto y finito: una lista, un rango, un generador. while hace algo diferente — repite un bloque mientras una condición sea verdadera, sin importar cuántas veces eso implique. El número de ciclos no está fijado de antemano; lo decide el programa en tiempo de ejecución.

El mecanismo es simple: Python evalúa la condición antes de cada iteración. Si es True, ejecuta el cuerpo. Si es False, sale del bucle y continúa. Eso significa que si la condición es False desde el principio, el cuerpo no se ejecuta ni una vez.

¿Cuándo necesitas esto? Cada vez que la condición de parada depende de algo que no puedes conocer antes de empezar: una respuesta del usuario, el estado de una conexión de red, si un archivo todavía tiene datos, si un reintento tuvo éxito. Para esos casos, for range(n) no sirve porque no sabes n.

Lo que se rompe cuando te equivocas es serio: un bucle infinito que congela tu programa. Ocurre cuando la condición nunca llega a ser False — ya sea porque olvidaste actualizar la variable que controla el bucle, porque la condición está mal escrita, o porque el evento que debería terminar el bucle nunca llega. Hay dos formas de prevenirlo: asegurarte de que el cuerpo del bucle siempre acerca el estado a la condición de salida, y añadir una condición de escape secundaria (un contador máximo, un timeout) como red de seguridad.

El idioma while True: con break explícito es perfectamente válido en Python — no es un code smell si la lógica de parada es genuinamente compleja o está en el medio del ciclo, no al inicio.

import time
import random

# ── 1. while con condición directa ─────────────────────────────────────
def countdown(start: int) -> None:
    n = start
    while n > 0:          # condición clara, n decrece en cada iteración
        print(n)
        n -= 1
    print("¡Despegue!")


# ── 2. while True + break: lectura de I/O hasta señal de fin ───────────
def read_until_quit() -> list[str]:
    lines: list[str] = []
    while True:
        line = input("Escribe algo (o 'fin' para terminar): ")
        if line.strip().lower() == "fin":
            break         # la condición de salida está en el medio, no al inicio
        lines.append(line)
    return lines


# ── 3. Reintento con backoff exponencial ───────────────────────────────
def fetch_with_retry(max_attempts: int = 5) -> str | None:
    attempt = 0
    delay = 1.0           # segundos iniciales de espera

    while attempt < max_attempts:
        success = random.random() > 0.7   # simula fallo con 70% de probabilidad
        if success:
            return f"Datos obtenidos en el intento {attempt + 1}"

        print(f"Intento {attempt + 1} fallido. Esperando {delay:.1f}s...")
        time.sleep(delay)
        delay *= 2        # duplica el tiempo de espera (backoff exponencial)
        attempt += 1

    return None           # agotamos los intentos


# ── 4. Máquina de estados simple ───────────────────────────────────────
def traffic_light_simulation(cycles: int = 2) -> None:
    states = ["verde", "amarillo", "rojo"]
    durations = {"verde": 0.5, "amarillo": 0.2, "rojo": 0.5}

    state_index = 0
    completed_cycles = 0

    while completed_cycles < cycles:
        current = states[state_index]
        print(f"  Semáforo: {current.upper()}")
        time.sleep(durations[current])

        state_index = (state_index + 1) % len(states)
        if state_index == 0:          # completamos un ciclo completo
            completed_cycles += 1


# ── Demo ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    print("--- Countdown ---")
    countdown(3)

    print("\n--- Reintento con backoff ---")
    result = fetch_with_retry()
    print(result or "No se pudo obtener datos.")

    print("\n--- Semáforo (2 ciclos) ---")
    traffic_light_simulation()

Qué hace cada parte y por qué importa

El countdown muestra la forma más pura: una variable que controla el bucle (n) y una operación en el cuerpo que la acerca a la condición de salida (n -= 1). Fíjate que podrías escribir esto con for n in range(start, 0, -1) — y en ese caso concreto, for sería más idiomático porque el número de iteraciones se puede calcular. Usa while aquí como punto de comparación.

El read_until_quit justifica while True:. La condición de parada — que el usuario escriba "fin" — no se puede evaluar hasta después de llamar a input(). No hay forma de saber antes de entrar al bucle si hay que salir. Poner la condición al inicio sería imposible. El break no es un truco sucio; es la herramienta correcta para este patrón de lectura.

El fetch_with_retry combina las dos ideas: la condición attempt < max_attempts garantiza que el bucle siempre termina (no hay riesgo de bucle infinito), y el backoff exponencial (delay *= 2) es un patrón real que usarás en cualquier cliente HTTP o sistema de colas. Si max_attempts no existiera y solo hicieras while not success:, tendrías un bucle potencialmente infinito si el servicio está caído para siempre.

La máquina de estados muestra cómo while encaja naturalmente cuando el número de iteraciones depende de transiciones de estado internas. El índice circular con % len(states) es la forma Pythonica de avanzar por una lista sin salirse de los límites. El while aquí controla cuántos ciclos completos queremos, mientras que la lógica interna maneja las transiciones. Esto escala bien: añadir un estado nuevo solo requiere tocar states y durations.

Errores que debes conocer

Error: Olvidar actualizar la variable de control, creando un bucle infinito accidental.

# ❌ Wrong
n = 5
while n > 0:
    print(n)
    # n nunca cambia → imprime 5 para siempre

# ✅ Right
n = 5
while n > 0:
    print(n)
    n -= 1    # cada iteración acerca n a la condición de salida

El cuerpo del while debe siempre modificar algo que afecte a la condición evaluada al inicio.


Error: Usar while donde for es la herramienta correcta, añadiendo complejidad innecesaria.

# ❌ Wrong
items = ["a", "b", "c"]
i = 0
while i < len(items):
    print(items[i])
    i += 1

# ✅ Right
items = ["a", "b", "c"]
for item in items:
    print(item)

Cuando iteras sobre una secuencia de longitud conocida, for elimina la variable de índice manual, el riesgo de off-by-one y la posibilidad de olvidar el incremento.


Error: while True: sin ninguna condición de escape garantizada.

# ❌ Wrong
def poll_sensor() -> int:
    while True:
        value = read_sensor()
        if value > 100:   # ¿y si el sensor está roto y nunca supera 100?
            return value

# ✅ Right
import time

def poll_sensor(timeout_seconds: float = 30.0) -> int | None:
    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        value = read_sensor()
        if value > 100:
            return value
        time.sleep(0.1)   # evita busy-waiting
    return None           # timeout alcanzado

Todo bucle de polling necesita un timeout como red de seguridad; time.monotonic() es preferible a time.time() porque no se ve afectado por ajustes del reloj del sistema.

51

Dejar un comentario

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

Scroll al inicio