Tiza Paso a paso
Practicar →

Pensamiento Computacional · Unidad 3 — Estructuras de control

Ciclo for y range

for repite una vez por cada elemento de algo. range fabrica esos elementos, y arranca en 0 y termina antes del número que le des.

Practicar este tema →8 ejercicios que te corrigen paso a paso · gratis, sin cuenta

La explicación

Recorrer algo

for letra in "sol":
    print(letra)

Imprime s, o, l. La variable letra toma cada valor, uno por vuelta. Su nombre lo elegís vos.

range fabrica números

range(5)         → 0, 1, 2, 3, 4
range(2, 7)     → 2, 3, 4, 5, 6
range(0, 10, 3)  → 0, 3, 6, 9
range(5, 0, -1)  → 5, 4, 3, 2, 1
Arranca en el primero, termina ANTES del segundo. range(5) no incluye el 5; da cinco números, del 0 al 4. Por eso range(n) sirve tan bien para recorrer algo de n elementos: los índices van justo de 0 a n−1.

Y de ahí sale el nombre del error más famoso de programación: el off-by-one, equivocarse por uno. Si tu ciclo procesa un elemento de más o de menos, empezá mirando el range.

Dos formas de recorrer

for c in palabra:            # los caracteres
    print(c)

for i in range(len(palabra)):  # las posiciones
    print(i, palabra[i])

La primera es más limpia y es la que conviene casi siempre. La segunda sólo cuando necesitás saber en qué posición estás.

Acumular

El patrón más usado de todos: una variable afuera que se va sumando adentro.

total = 0                # afuera, antes de empezar
for n in range(1, 6):
    total = total + n   # adentro, una vez por vuelta
print(total)            # 15
El acumulador va afuera del ciclo. Si lo ponés adentro se reinicia en cada vuelta y al final te queda sólo el último valor. Es un error que no tira ningún mensaje: simplemente el resultado está mal, y cuesta verlo.

Un range que no da vueltas

range(3, 3) está vacío: el ciclo no se ejecuta ni una vez. No es un error, y explica esos casos raros donde "el for no hace nada".

Un ejercicio resuelto, paso a paso

Escribir una función que reciba un número n y devuelva la suma de todos los pares desde 1 hasta n.

Las tres decisiones. Qué recorro, qué acumulo, y cuándo sumo.

def suma_pares(n):
    total = 0                    # acumulador, afuera
    for i in range(1, n + 1):  # el n+1 es para que n entre
        if i % 2 == 0:           # sólo los pares
            total = total + i
    return total

Por qué n + 1. Como range termina antes del segundo número, range(1, n) dejaría afuera justo a n. Con n + 1 el último valor que entra es n. Es el off-by-one en su forma más común.

Probemos con n = 10: suma 2 + 4 + 6 + 8 + 10 = 30.

Hay una versión más directa usando el paso de range, que arranca en 2 y va de a 2:
def suma_pares(n):
    total = 0
    for i in range(2, n + 1, 2):
        total = total + i
    return total
Hace la mitad de vueltas y no necesita el if. Cuando puedas expresar la condición en el propio range, sale mejor.

Control: con n = 2 tiene que dar 2, y con n = 1 tiene que dar 0. Los casos chicos son los que descubren los off-by-one.

Y uno del nivel del parcial

Escribir una función que reciba un número n y devuelva su cantidad de divisores, y otra que use la primera para decidir si n es primo. Probar con 12, 7, 1 y 97, y explicar por qué la versión directa es lenta.

La definición de divisor, traducida a código: i divide a n cuando el resto de la división es cero. Con eso, contar divisores es recorrer y contar:

def divisores(n):
    cantidad = 0
    for i in range(1, n + 1):
        if n % i == 0:
            cantidad = cantidad + 1
    return cantidad

El n + 1 del range es para que n se cuente a sí mismo: todo número se divide por sí mismo, y si no entra, todos los resultados salen uno menos.

La función de primos, que se apoya en la anterior:

def es_primo(n):
    return n > 1 and divisores(n) == 2

La definición de primo es exactamente "tiene dos divisores": el 1 y él mismo. Escribirla así evita la discusión sobre el 1, porque el 1 tiene un solo divisor y queda afuera solo.

El n > 1 del principio es para los negativos y el cero, donde divisores devolvería 0 y la comparación no tendría sentido.

Las pruebas:

divisores(12) → 6   (1, 2, 3, 4, 6, 12)
divisores(7)  → 2   (1, 7)
divisores(1)  → 1   (solo el 1)
divisores(36) → 9   (1,2,3,4,6,9,12,18,36)
es_primo(1)  → False   (el caso que más se equivoca)
es_primo(2)  → True    (el único primo par)
es_primo(9)  → False   (9 = 3·3, no se salva por ser impar)
es_primo(97) → True

El control fuerte: los primos hasta 30 tienen que ser 2, 3, 5, 7, 11, 13, 17, 19, 23 y 29. Son diez, y el programa devuelve esos diez ✓. Una lista conocida es la mejor prueba para una función de este tipo.

Ahora por qué es lenta. Para decidir si 1.000.003 es primo, el ciclo da un millón de vueltas. Y casi todas son inútiles.

La observación que arregla todo: los divisores vienen de a pares. Si i divide a n, entonces n/i también:

36 = 1·36 = 2·18 = 3·12 = 4·9 = 6·6

El par se "da vuelta" en la raíz cuadrada. Después de 6, los divisores que aparecen son los compañeros de los que ya vimos. Así que alcanza con buscar hasta √n y contar de a dos:

def divisores_rapido(n):
    total = 0
    i = 1
    while i * i <= n:
        if n % i == 0:
            if i * i == n:
                total = total + 1 # el del medio, una sola vez
            else:
                total = total + 2 # i y su companero n // i
        i = i + 1
    return total

El if i * i == n: cuando n es un cuadrado perfecto, el divisor del medio (el 6 de 36) es su propio compañero y se cuenta una sola vez. Sin esa línea, 36 daría 10 divisores en vez de 9.

Por qué i*i <= n y no i <= math.sqrt(n): multiplicar enteros es exacto; la raíz cuadrada es un número con coma y puede dar 5.999999 donde debería dar 6, y ahí se pierde un divisor.

La diferencia en la práctica: para 1.000.003 la primera versión da un millón de vueltas y la segunda da mil. Las dos versiones coinciden en todos los números del 1 al 400, así que la rápida no es "otra cosa": es la misma función, mil veces más rápida.

Dónde se pierde el punto: contestar que el 1 es primo, o escribir range(2, n) y perder el divisor n. Y en la versión rápida, olvidar el caso del cuadrado perfecto: es el único número que rompe el conteo de a pares.

Dónde se cae la mayoría

Hasta acá la explicación. La práctica es la otra mitad: 8 ejercicios de este tema, en cuatro niveles, que no te dicen sólo si está bien al final sino que te corrigen en cada paso y te explican por qué. Y uno para escribir el programa vos, que se prueba ejecutándolo en Python ahí mismo, en el navegador. Además trae una animación que muestra la idea en movimiento.

Practicar Ciclo for y range →

Dónde entra este tema en el parcial

Los otros temas de la unidad 3

Estructuras de control

Ver todos los temas de Pensamiento Computacional