UdelaR  ·  Máster en Análisis de Datos

Introducción a Python

Clase 5 — Bucles e iteración

Asignatura  Introducción a Python Profesor  Daniel Miles Touya Universidad  UdelaR-Uvigo
Parte I

El bucle: for

El bucle for y range() teoría

El bucle for repite un bloque de código para cada elemento de una secuencia o colección. Es el mecanismo de repetición más usado en Python — y en análisis de datos aparece constantemente para procesar listas de registros, columnas de un DataFrame o resultados de una consulta.

La sintaxis es: for variable in iterable: seguido del bloque indentado. En cada iteración, variable toma el valor del siguiente elemento del iterable.

— for sobre distintos iterables —

# for sobre una lista
frutas = ["manzana", "pera", "naranja"]
for fruta in frutas:
    print(fruta)

# for sobre un string — carácter a carácter
for letra in "Python":
    print(letra, end=" ")
print()

# for sobre una tupla
coordenadas = (40.4168, -3.7038)
for valor in coordenadas:
    print(valor)

# El nombre de la variable lo eliges tú
# Debe ser descriptivo — no siempre "i"
ventas = [150, 80, 200, 95]
for cantidad in ventas:
    print(f"vendidas: {cantidad}")

— La forma pythónica: iterar sobre elementos, no índices —

En otros lenguajes es habitual recorrer una lista usando el índice: for i in range(len(lista)) y luego acceder con lista[i]. En Python esto funciona pero es innecesariamente verboso — si solo necesitas el valor, itera directamente sobre los elementos. El código es más legible y menos propenso a errores de índice.

nombres = ["Ana", "Bob", "Carlos"]

# ❌ Forma con índice — innecesaria si solo quieres el valor
for i in range(len(nombres)):
    print(nombres[i])

# ✓ Forma pythónica — directa y legible
for nombre in nombres:
    print(nombre)

# Acumulación — sumar elementos
total = 0
for cantidad in ventas:
    total += cantidad
print(f"Total: {total}")   # equivale a sum(ventas)

# Ignorar la variable con _ cuando no la necesitas
for _ in range(3):
    print("meow")

— range(): tres formas —

range() genera una secuencia de números enteros. Es un iterable — no crea una lista en memoria, produce los números uno a uno. Tiene tres formas según los argumentos:

range(stop) — de 0 a stop-1. range(start, stop) — de start a stop-1. range(start, stop, step) — de start a stop-1 con salto step. El stop siempre es exclusivo — igual que en el slicing.

# range(stop) — de 0 a n-1
for i in range(5):
    print(i, end=" ")   # 0 1 2 3 4
print()

# range(start, stop) — de start a stop-1
for i in range(1, 6):
    print(i, end=" ")   # 1 2 3 4 5
print()

# range(start, stop, step) — con salto
for i in range(0, 10, 2):
    print(i, end=" ")   # 0 2 4 6 8 — pares
print()

# Cuenta atrás — step negativo
for i in range(5, 0, -1):
    print(i, end=" ")   # 5 4 3 2 1
print()

# Suma de los primeros 100 números
total = 0
for i in range(1, 101):
    total += i
print(f"Suma 1..100: {total}")   # 5050

# Convertir range a lista cuando necesitas verlo
print(list(range(1, 10, 3)))   # [1, 4, 7]
  1. Predice qué imprime este código antes de ejecutarlo:
    for x in [1, 2, 3]:
        print(x * 2)
    
    for c in "hola":
        print(c.upper(), end="")
    print()
    
    total = 0
    for n in range(1, 5):
        total += n
    print(total)
  2. ¿Qué números genera cada range()? Predice antes de convertir a lista:
    print(list(range(5)))
    print(list(range(2, 8)))
    print(list(range(0, 20, 4)))
    print(list(range(10, 0, -2)))
  3. Dado este código que usa índices, reescríbelo en la forma pythónica sin usar índices:
    precios = [12.5, 8.0, 15.3, 9.7]
    for i in range(len(precios)):
        print(f"precio: {precios[i]:.2f}€")
  4. Escribe un bucle for con range() que calcule la suma de todos los números impares entre 1 y 99 inclusive. ¿Puedes hacerlo en dos líneas?
Respuesta orientativa
a

Primer bucle: 2, 4, 6. Segundo bucle: HOLA — cada carácter en mayúscula, sin espacios por el end="", con salto de línea al final por el print() suelto. Tercer bucle: total = 10 — suma de 1+2+3+4.

b

range(5) → [0, 1, 2, 3, 4]. range(2, 8) → [2, 3, 4, 5, 6, 7]. range(0, 20, 4) → [0, 4, 8, 12, 16]. range(10, 0, -2) → [10, 8, 6, 4, 2].

El stop siempre es exclusivo: range(2, 8) llega hasta el 7, no el 8. Igual que en el slicing, el último valor nunca se incluye.
c
precios = [12.5, 8.0, 15.3, 9.7]
for precio in precios:
    print(f"precio: {precio:.2f}€")

La versión sin índice es más corta, más legible y no puede producir un IndexError por error en el rango. El nombre precio (singular) en lugar de precios (plural) es la convención habitual — la variable del bucle es un elemento de la colección.

d
total = 0
for n in range(1, 100, 2):
    total += n
print(total)   # 2500

range(1, 100, 2) genera los impares del 1 al 99. El stop es 100 — exclusivo — así que el último valor incluido es 99. Más conciso aún: sum(range(1, 100, 2)) — una sola línea sin bucle explícito.

enumerate() y zip(): iterar con más información práctica

La forma pythónica de iterar es directamente sobre los elementos — sin índices. Pero a veces sí necesitas el índice junto con el valor, o recorrer dos listas en paralelo. Para eso Python tiene enumerate() y zip(): dos funciones que producen iterables enriquecidos sin necesidad de gestionar índices manualmente.

— enumerate(): índice y valor juntos —

enumerate(iterable) devuelve pares (índice, valor) para cada elemento del iterable. Permite usar el índice cuando lo necesitas sin tener que escribir range(len(lista)) ni acceder con lista[i]. Opcionalmente puedes especificar el índice inicial con enumerate(iterable, start=1).

nombres = ["Ana", "Bob", "Carlos", "Diana"]

# ❌ Forma con range(len()) — verboso
for i in range(len(nombres)):
    print(f"{i}: {nombres[i]}")

print("---")

# ✓ Con enumerate — más claro
for i, nombre in enumerate(nombres):
    print(f"{i}: {nombre}")

print("---")

# Empezar desde 1 en lugar de 0
for pos, nombre in enumerate(nombres, start=1):
    print(f"Puesto {pos}: {nombre}")

print("---")

# enumerate devuelve tuplas — se puede verificar
print(list(enumerate(nombres)))
print(list(enumerate(nombres, start=1)))

enumerate() devuelve tuplas (índice, valor) — por eso se usa el unpacking de tuplas en for i, nombre in enumerate(...). Conecta directamente con el unpacking de la Clase 3.

— zip(): recorrer varios iterables en paralelo —

zip(iter1, iter2, ...) combina varios iterables emparejando sus elementos por posición — el primer elemento de cada uno, el segundo de cada uno, y así sucesivamente. Devuelve tuplas con un elemento de cada iterable. Se detiene cuando el iterable más corto se agota.

nombres = ["Ana", "Bob", "Carlos"]
notas   = [8.5, 7.2, 9.1]

# ❌ Forma con índice — propensa a errores
for i in range(len(nombres)):
    print(f"{nombres[i]}: {notas[i]}")

print("---")

# ✓ Con zip — limpio y seguro
for nombre, nota in zip(nombres, notas):
    print(f"{nombre}: {nota}")

print("---")

# zip con tres iterables
productos  = ["manzana", "pera", "naranja"]
precios    = [1.20, 0.90, 0.80]
cantidades = [50, 30, 40]

for prod, precio, cantidad in zip(productos, precios, cantidades):
    print(f"{prod:10} {precio:.2f}€ × {cantidad} = {precio*cantidad:.2f}€")

# zip se detiene en el más corto
largo  = [1, 2, 3, 4, 5]
corto  = ["a", "b", "c"]
print(list(zip(largo, corto)))   # solo 3 pares

— Patrones frecuentes con zip y enumerate —

# zip para crear un diccionario — ya lo vimos en Clase 3
claves  = ["nombre", "edad", "ciudad"]
valores = ["Ana", 28, "Madrid"]
persona = dict(zip(claves, valores))
print(persona)

# enumerate para construir un dict con posición
frutas = ["manzana", "pera", "naranja"]
indice_frutas = dict(enumerate(frutas, start=1))
print(indice_frutas)   # {1: 'manzana', 2: 'pera', 3: 'naranja'}

# zip + enumerate — posición y par de valores
nombres = ["Ana", "Bob", "Carlos"]
notas   = [8.5, 7.2, 9.1]
for pos, (nombre, nota) in enumerate(zip(nombres, notas), start=1):
    print(f"{pos}. {nombre}: {nota}")

# Conexión con pandas: zip es la base de df.iterrows()
import pandas as pd
df = pd.DataFrame({"producto": productos, "precio": precios})
for idx, fila in df.iterrows():
    print(f"{idx}: {fila['producto']} — {fila['precio']:.2f}€")
  1. Predice qué imprime este código antes de ejecutarlo:
    colores = ["rojo", "verde", "azul"]
    for i, color in enumerate(colores, start=1):
        print(f"Color {i}: {color}")
    
    a = [1, 2, 3]
    b = ["x", "y"]
    for n, letra in zip(a, b):
        print(n, letra)
  2. ¿Por qué zip(largo, corto) produce solo 3 pares aunque largo tenga 5 elementos? ¿Qué pasaría si las listas de nombres y notas tuvieran distinta longitud en el ejemplo del aula?
  3. Reescribe este código usando enumerate() empezando desde 1:
    alumnos = ["Ana", "Bob", "Carlos", "Diana"]
    for i in range(len(alumnos)):
        print(f"Alumno {i+1}: {alumnos[i]}")
  4. Tienes estas tres listas. Usando zip() calcula el ingreso total de cada producto (precio × cantidad) e imprímelo con formato:
    productos  = ["manzana", "pera", "naranja", "kiwi"]
    precios    = [1.20, 0.90, 0.80, 1.50]
    cantidades = [150, 80, 120, 40]
Respuesta orientativa
a

Primer bucle: Color 1: rojo, Color 2: verde, Color 3: azul. Segundo bucle: 1 x, 2 y — solo dos pares porque b tiene dos elementos. El 3 de a no tiene par y se descarta.

b

zip() se detiene cuando el iterable más corto se agota — no produce un error, simplemente descarta los elementos sobrantes del más largo. Si las listas de nombres y notas tuvieran distinta longitud, algunos alumnos no tendrían nota o algunas notas no tendrían alumno — silenciosamente, sin aviso.

Verificación importante: antes de usar zip() sobre dos listas que deberían tener la misma longitud, verifica que la tienen: assert len(nombres) == len(notas). Si no coinciden es casi siempre un error en los datos.
c
alumnos = ["Ana", "Bob", "Carlos", "Diana"]
for i, alumno in enumerate(alumnos, start=1):
    print(f"Alumno {i}: {alumno}")

La versión con enumerate elimina el i+1 — el ajuste manual del índice desaparece porque start=1 lo gestiona directamente.

d
productos  = ["manzana", "pera", "naranja", "kiwi"]
precios    = [1.20, 0.90, 0.80, 1.50]
cantidades = [150, 80, 120, 40]

for prod, precio, cantidad in zip(productos, precios, cantidades):
    ingreso = precio * cantidad
    print(f"{prod:10} {ingreso:.2f}€")

# manzana    180.00€
# pera        72.00€
# naranja     96.00€
# kiwi        60.00€
Patrón frecuente en datos: zip() sobre tres o más listas paralelas es exactamente lo que hace pandas internamente cuando itera sobre columnas de un DataFrame. Entender zip() es entender la base de la iteración sobre tablas.
break, continue y else en el bucle práctica

Por defecto un for recorre todos los elementos del iterable de principio a fin. Pero hay tres mecanismos que modifican ese comportamiento: break sale del bucle antes de terminar, continue salta a la siguiente iteración, y else ejecuta un bloque solo si el bucle terminó sin break.

— break: salir del bucle antes de terminar —

break detiene el bucle inmediatamente y salta a la primera línea después del bloque del for. Es útil cuando ya encontraste lo que buscabas y no tiene sentido seguir recorriendo el resto.

# Buscar el primer número negativo
datos = [5, 12, 3, -7, 8, -2, 4]

for valor in datos:
    print(f"revisando {valor}")
    if valor < 0:
        print(f"primer negativo: {valor}")
        break   # salimos — no hace falta seguir

print("búsqueda terminada")

# Sin break — recorrería todos los elementos siempre
# Con break — se detiene en el primero que cumple la condición

— continue: saltar a la siguiente iteración —

continue no sale del bucle — salta el resto del bloque de la iteración actual y pasa directamente a la siguiente. Es útil para filtrar elementos que no quieres procesar.

# Imprimir solo los impares — saltar los pares
for n in range(1, 11):
    if n % 2 == 0:
        continue   # salta los pares
    print(n, end=" ")
print()

# Procesar solo registros válidos
registros = ["Ana", None, "Bob", "", "Carlos"]
for nombre in registros:
    if not nombre:
        continue   # salta None y string vacío
    print(f"procesando: {nombre}")

print("---")

# Comparación: break vs continue
print("Con break:")
for i in range(5):
    if i == 2:
        break
    print(i, end=" ")   # 0 1
print()

print("Con continue:")
for i in range(5):
    if i == 2:
        continue
    print(i, end=" ")   # 0 1 3 4
print()

break

Sale del bucle completamente. El resto de elementos no se procesa. Útil cuando ya encontraste lo que buscabas.

continue

Salta solo la iteración actual. El bucle continúa con el siguiente elemento. Útil para filtrar elementos a ignorar.

— else en for: el bloque que se ejecuta sin break —

El else de un for se ejecuta cuando el bucle termina de forma natural — sin haber sido interrumpido por un break. Si el bucle terminó con break, el else no se ejecuta.

Es un patrón que aparece frecuentemente en código generado por IA para el caso "buscar algo y actuar si no se encontró". Mucha gente no lo conoce y lo confunde con el else de un if.

# Buscar un valor — actuar si no se encuentra
numeros = [5, 12, 3, 8, 4]
buscado = 7

for n in numeros:
    if n == buscado:
        print(f"encontrado: {buscado}")
        break
else:
    print(f"{buscado} no está en la lista")

print("---")

# Con el valor presente — el else NO se ejecuta
buscado = 12
for n in numeros:
    if n == buscado:
        print(f"encontrado: {buscado}")
        break
else:
    print(f"{buscado} no está en la lista")

print("---")

# Sin else — alternativa más común con variable bandera
encontrado = False
for n in numeros:
    if n == buscado:
        encontrado = True
        break

if not encontrado:
    print(f"{buscado} no encontrado")
else:
    print(f"{buscado} encontrado")
  1. Predice qué imprime cada versión antes de ejecutar:
    for i in range(6):
        if i == 3:
            break
        print(i, end=" ")
    
    print()
    
    for i in range(6):
        if i == 3:
            continue
        print(i, end=" ")
  2. En el bloque de continue con registros: ¿qué imprime si la lista es [None, None, None]? ¿Y si es ["Ana"]? ¿Cuántas líneas imprime en cada caso?
  3. ¿Cuándo se ejecuta el else de un for y cuándo no? Predice qué imprime este código:
    valores = [2, 4, 6, 8]
    for v in valores:
        if v % 2 != 0:
            print("encontrado impar")
            break
    else:
        print("todos son pares")
  4. Este código busca el primer precio por encima de un umbral. ¿Hay algún problema? ¿Cómo lo mejorarías con break y else?
    precios = [12.5, 8.0, 15.3, 9.7, 11.2]
    umbral = 14.0
    encontrado = None
    
    for p in precios:
        if p > umbral:
            encontrado = p
    
    if encontrado:
        print(f"precio alto: {encontrado}")
    else:
        print("ningún precio supera el umbral")
Respuesta orientativa
a

Con break: imprime 0 1 2 — cuando i == 3 el bucle se detiene antes de imprimir el 3. Con continue: imprime 0 1 2 4 5 — cuando i == 3 salta esa iteración pero continúa con 4 y 5.

b

Con [None, None, None]: no imprime nada — todos los elementos son falsy y el continue salta todos. Con ["Ana"]: imprime una línea: "procesando: Ana".

c

El else se ejecuta cuando el bucle termina sin break. Con valores = [2, 4, 6, 8] ningún valor es impar — el bucle recorre todos los elementos sin ejecutar el break → el else se ejecuta → imprime "todos son pares".

Patrón de lectura: el else del for se puede leer como "si no encontré nada que me hiciera salir". Es el complemento natural del break.
d

El problema es que el código no se detiene al encontrar el primer precio alto — sigue recorriendo toda la lista y guarda el último precio que supera el umbral, no el primero. Con break y else:

precios = [12.5, 8.0, 15.3, 9.7, 11.2]
umbral = 14.0

for p in precios:
    if p > umbral:
        print(f"primer precio alto: {p}")
        break
else:
    print("ningún precio supera el umbral")
Error silencioso frecuente: iterar sin break cuando solo necesitas el primer resultado — el bucle sigue procesando innecesariamente y puede sobreescribir el resultado correcto con uno posterior.
Parte II

Iteradores e iterables

Iterables e iteradores: qué hace el for por debajo teoría

En la Parte I usamos el for sobre listas, strings, tuplas y rangos. Todos funcionan — pero son objetos muy distintos. ¿Por qué el for funciona sobre todos ellos? ¿Y por qué no funciona sobre un número entero?

La respuesta está en dos conceptos que están por debajo de todo lo que hemos visto: iterable e iterador. No hace falta usarlos directamente en el día a día — pero entenderlos explica el comportamiento del for, los errores que produce y por qué funciona sobre estructuras tan distintas.

— Qué es un iterable —

Un iterable es cualquier objeto del que Python puede extraer elementos uno a uno. No significa que los elementos estén almacenados todos en memoria — significa que el objeto sabe cómo producirlos en orden cuando se le pide.

¿Cómo sabe Python si un objeto es iterable? Porque tiene un método especial llamado __iter__(). Si lo tiene, es iterable y el for puede recorrerlo. Si no lo tiene, el for lanza un TypeError.

# Comprobar qué objetos son iterables
objetos = {
    "lista":  [1, 2, 3],
    "tupla":  (4, 5, 6),
    "string": "Python",
    "range":  range(5),
    "dict":   {"a": 1},
    "set":    {1, 2},
    "int":    42,
    "float":  3.14,
}

for nombre, obj in objetos.items():
    tiene = hasattr(obj, "__iter__")
    print(f"{nombre:8} → iterable: {tiene}")

# Un entero no es iterable — TypeError
try:
    for x in 42:
        print(x)
except TypeError as e:
    print(f"\nTypeError: {e}")

— Qué es un iterador —

Un iterador es el objeto que hace el trabajo de recorrer el iterable. Guarda la posición actual — cuál fue el último elemento entregado — y sabe cuál es el siguiente.

Se crea desde un iterable con iter(). Se avanza con next() — cada llamada devuelve el siguiente elemento. Cuando no quedan más elementos, next() lanza StopIteration.

La diferencia clave entre iterable e iterador: el iterable es la fuente de datos — puedes recorrerlo tantas veces como quieras creando iteradores nuevos. El iterador es el cursor que recorre — es de un solo uso, una vez agotado no se puede reiniciar.

# El iterable y el iterador son objetos distintos
lista = ["manzana", "pera", "naranja"]
iterador = iter(lista)

print(type(lista))       # list       — el iterable
print(type(iterador))    # list_iterator — el iterador
print()

# Avanzar manualmente con next()
print(next(iterador))    # "manzana"
print(next(iterador))    # "pera"
print(next(iterador))    # "naranja"

# Agotado — StopIteration
try:
    print(next(iterador))
except StopIteration:
    print("iterador agotado")

# El iterable original sigue intacto
print(f"\nlista original: {lista}")

# Se puede crear un iterador nuevo desde el mismo iterable
iterador2 = iter(lista)
print(next(iterador2))   # "manzana" — empieza desde el inicio
Iterador de un solo uso: una vez agotado un iterador, no se puede reiniciar — hay que crear uno nuevo con iter(). El iterable original no se modifica. Si un bucle for consume un iterador hasta el final, el mismo iterador no producirá nada en un segundo for.
# Demostración: iterador de un solo uso
lista = [1, 2, 3]
it = iter(lista)

print("Primer recorrido del iterador:")
for x in it:
    print(x, end=" ")
print()

print("Segundo recorrido del mismo iterador:")
for x in it:
    print(x, end=" ")    # no imprime nada — agotado
print("(vacío)")

print("Recorrido del iterable original:")
for x in lista:
    print(x, end=" ")    # imprime todo — nuevo iterador
print()

— Qué hace el for por debajo —

El for no es magia. Por debajo hace exactamente tres cosas: llama a iter() sobre el iterable para obtener un iterador, llama a next() repetidamente asignando cada valor a la variable del bucle, y se detiene cuando captura StopIteration.

frutas = ["manzana", "pera", "naranja"]

# Lo que escribe el programador
for fruta in frutas:
    print(fruta)

print("---")

# Lo que Python hace por debajo — equivalente exacto
_iter = iter(frutas)        # paso 1: obtener el iterador
while True:
    try:
        fruta = next(_iter)  # paso 2: pedir el siguiente
        print(fruta)          # paso 3: ejecutar el bloque
    except StopIteration:
        break                 # paso 4: parar cuando se acaba
Por qué esto importa: entender que el for usa iter() y next() explica por qué funciona sobre cualquier objeto que tenga __iter__() — listas, strings, ficheros, respuestas de APIs, DataFrames de pandas. Cuando ves un TypeError: object is not iterable ya sabes exactamente qué significa: ese objeto no tiene __iter__().

— range: iterable que no almacena en memoria —

range(1_000_000) no crea una lista de un millón de números en memoria. Solo guarda el inicio, el fin y el paso — tres números. Produce cada número en el momento en que next() lo pide. Esto lo hace eficiente incluso con rangos enormes. A este patrón — producir valores bajo demanda en lugar de almacenarlos todos — se le llama evaluación perezosa o lazy evaluation.

import sys

# Comparación de memoria
lista_grande = list(range(1_000_000))
rango_grande = range(1_000_000)

print(f"list(range): {sys.getsizeof(lista_grande):>10} bytes")
print(f"range:       {sys.getsizeof(rango_grande):>10} bytes")

# range es iterable — funciona con for e iter/next
it = iter(rango_grande)
print(next(it))   # 0
print(next(it))   # 1
print(next(it))   # 2

# range es iterable pero no iterador — se puede recorrer varias veces
print(list(rango_grande[:3]))   # [0, 1, 2]
print(list(rango_grande[:3]))   # [0, 1, 2] — sigue intacto
  1. Ejecuta el bloque de iterables. ¿Qué objetos tienen __iter__? ¿Qué error produce intentar recorrer un float con for? ¿Es el mismo error que con un int?
  2. Crea un iterador manualmente desde el string "datos" usando iter(). Llama a next() cinco veces. ¿Qué produce la sexta llamada? ¿Qué tipo tiene el iterador?
  3. Dado este código, predice qué imprime antes de ejecutarlo:
    nums = [10, 20, 30]
    it = iter(nums)
    print(next(it))
    for x in it:
        print(x)
    for x in nums:
        print(x)
  4. Ejecuta el bloque de memoria. ¿Cuántos bytes ocupa la lista de un millón de números? ¿Y el range? ¿Qué implica esto cuando necesitas generar índices para iterar sobre un DataFrame de un millón de filas?
Respuesta orientativa
a

Tienen __iter__: lista, tupla, string, range, dict y set. No lo tienen: int y float. El error al intentar recorrer un float es el mismo que con int: TypeError: 'float' object is not iterable. El mensaje es exactamente el mismo patrón — el nombre del tipo cambia pero la causa es la misma: el objeto no tiene __iter__().

b
it = iter("datos")
print(next(it))   # 'd'
print(next(it))   # 'a'
print(next(it))   # 't'
print(next(it))   # 'o'
print(next(it))   # 's'
print(next(it))   # StopIteration

El tipo del iterador es str_iterator. La sexta llamada lanza StopIteration porque el string tiene exactamente 5 caracteres y el iterador los ha entregado todos.

c

Imprime: 10, 20, 30, 10, 20, 30.

El primer next(it) consume el 10. El for x in it continúa desde donde quedó el iterador — imprime 20 y 30. El segundo for x in nums itera sobre el iterable original — que sigue intacto — creando un nuevo iterador desde el inicio.

La trampa: un iterador y el iterable del que proviene son objetos distintos. Consumir el iterador no modifica el iterable. El for sobre el iterable siempre crea un iterador nuevo desde el inicio.
d

La lista ocupa alrededor de 8 millones de bytes (≈8 MB). El range ocupa 48 bytes — independientemente del tamaño del rango.

Si necesitas generar índices para iterar sobre un DataFrame de un millón de filas, range(len(df)) ocupa 48 bytes mientras que list(range(len(df))) ocupa 8 MB. En general, usa range siempre que solo necesites recorrer los valores — no almacenarlos.

Evaluación perezosa: producir valores bajo demanda en lugar de calcularlos todos de antemano es uno de los principios de eficiencia más importantes en Python. En la Clase 6 veremos yield — que permite crear tus propios iterables perezosos.
Parte III

El bucle while

El bucle while: repetir hasta que una condición cambie teoría

El bucle while repite un bloque de código mientras una condición sea True. Antes de cada iteración evalúa la condición — si es True ejecuta el bloque, si es False sale del bucle.

La diferencia fundamental con el for: el for recorre un iterable — sabes de antemano cuántas iteraciones habrá. El while se repite hasta que una condición cambia — no sabes cuántas iteraciones habrá hasta que ocurre.

Regla práctica: usa for cuando iteras sobre una colección o un rango conocido. Usa while cuando la condición de parada depende de algo que ocurre durante la ejecución.

— while básico —

# Contar hacia atrás
i = 5
while i > 0:
    print(i, end=" ")
    i -= 1   # imprescindible — si no, bucle infinito
print("despegue")

# Mismo resultado con for — más conciso cuando el rango es conocido
for i in range(5, 0, -1):
    print(i, end=" ")
print("despegue")

# while cuando no sabes cuántas iteraciones habrá
# Duplicar hasta superar un umbral
valor = 1
pasos = 0
while valor < 1000:
    valor *= 2
    pasos += 1
print(f"valor: {valor}, pasos: {pasos}")
# No podríamos usar for aquí — no sabemos cuántos pasos necesitamos

— El bucle infinito: el error más frecuente con while —

Un bucle while que nunca modifica su condición de parada se ejecuta para siempre — el programa se queda colgado. Es el error más frecuente con while y uno de los más fáciles de cometer.

Antes de escribir un while hazte siempre esta pregunta: ¿qué cambia dentro del bucle para que la condición eventualmente sea False? Si no hay respuesta, hay un bucle infinito.

# ❌ Bucle infinito — i nunca cambia
# i = 3
# while i != 0:
#     print("meow")   # se ejecuta para siempre

# ✓ Correcto — i se actualiza en cada iteración
i = 3
while i != 0:
    print("meow")
    i -= 1   # sin esta línea → bucle infinito

# Forma alternativa más clara
i = 0
while i < 3:
    print("meow")
    i += 1

# Verificar qué cambia en cada iteración
stock = 10
ventas_por_dia = [3, 4, 2, 5, 1]
dia = 0

while stock > 0 and dia < len(ventas_por_dia):
    venta = ventas_por_dia[dia]
    stock -= venta
    print(f"día {dia+1}: -{venta} → stock: {max(0, stock)}")
    dia += 1

print(f"Stock final: {max(0, stock)}")

— while True con break: condiciones de salida complejas —

while True crea un bucle que por definición no termina solo — la condición siempre es True. La única forma de salir es con break dentro del bucle. Es útil cuando la condición de salida es compleja o está en el medio del bloque, no al inicio.

# Simular reintentos hasta éxito
# (en lugar de input() usamos una lista de intentos)
intentos = [-3, 0, -1, 5]   # simulamos las respuestas del usuario
idx = 0

while True:
    n = intentos[idx]
    idx += 1
    print(f"valor introducido: {n}")
    if n > 0:
        print(f"valor válido: {n}")
        break
    print("debe ser positivo — intenta de nuevo")

print("procesando...")

# Equivalente con while condición — menos claro en este caso
idx = 0
n = intentos[idx]
while n <= 0:
    print(f"valor inválido: {n}")
    idx += 1
    n = intentos[idx]
print(f"valor válido: {n}")
Cuándo usar while True: cuando la condición de salida está en el medio del bloque — no al principio. Si la condición está al principio, es más claro usar while condición directamente. Si está al final o en el medio, while True con break expresa mejor la intención.
  1. Predice qué imprime este código antes de ejecutarlo:
    n = 1
    while n <= 32:
        print(n, end=" ")
        n *= 2
    print("fin")
  2. ¿Qué tiene de malo este código? ¿Qué pasaría si lo ejecutaras? ¿Cómo lo corregirías?
    total = 0
    i = 1
    while i <= 10:
        total += i
    print(total)
  3. ¿Cuándo usarías for y cuándo while para cada caso? Justifica tu elección:
    — Recorrer una lista de productos
    — Esperar hasta que el stock baje de un umbral
    — Calcular la suma de los primeros 100 números
    — Procesar registros hasta encontrar uno con error
  4. Dado este fragmento con while True, predice cuántas iteraciones se ejecutan y qué imprime:
    valores = [8, 3, 15, 2, 11]
    idx = 0
    acum = 0
    
    while True:
        acum += valores[idx]
        idx += 1
        if acum > 20 or idx >= len(valores):
            break
    
    print(f"acumulado: {acum}, iteraciones: {idx}")
Respuesta orientativa
a

Imprime: 1 2 4 8 16 32 fin. La condición es n <= 32 — cuando n vale 32 todavía se imprime porque la condición es verdadera. En la siguiente iteración n pasa a 64 y la condición es falsa — sale del bucle e imprime "fin".

b

Falta actualizar i dentro del bucle — es un bucle infinito. La condición i <= 10 siempre es True porque i nunca cambia. El programa se quedaría colgado sumando 1 para siempre. La corrección:

total = 0
i = 1
while i <= 10:
    total += i
    i += 1   # ← faltaba esta línea
print(total)   # 55
c

Recorrer una lista de productos → for: iterable conocido, número de iteraciones fijo.

Esperar hasta que el stock baje de un umbral → while: la condición de parada depende de lo que ocurre durante la ejecución, no se sabe cuántas iteraciones habrá.

Suma de los primeros 100 números → for con range(1, 101): el rango es conocido. También se puede con sum(range(1, 101)) sin ningún bucle explícito.

Procesar hasta encontrar un error → while o for con break: si la lista es conocida, for con break es más claro. Si los registros llegan de forma continua (stream), while.

d

Iteración 1: acum = 8, idx = 1 — condición: 8 > 20 es False, 1 >= 5 es False → continúa. Iteración 2: acum = 11, idx = 2 → continúa. Iteración 3: acum = 26, idx = 3 — condición: 26 > 20 es True → break.

Imprime: acumulado: 26, iteraciones: 3. Se ejecutaron 3 iteraciones.

Patrones frecuentes con while: validación, reintentos y acumulación práctica

break y continue funcionan en while exactamente igual que en for: break sale del bucle, continue salta al inicio de la siguiente iteración.

Pero el while tiene sus propios patrones de uso frecuentes que aparecen constantemente en código real y en código generado por IA: validación — repetir hasta que un dato sea correcto, reintentos — intentar una operación hasta que funcione, y acumulación con condición — acumular valores hasta que se cumpla una condición de parada.

— Patrón 1: validación — repetir hasta dato correcto —

# Simular validación de contraseña
# (simulamos intentos con una lista en lugar de input())
intentos = ["1234", "qwerty", "python123"]
correcta = "python123"
idx = 0
max_intentos = 3

while idx < max_intentos:
    clave = intentos[idx]
    idx += 1
    print(f"intento {idx}: '{clave}'")
    if clave == correcta:
        print("acceso concedido")
        break
    print(f"contraseña incorrecta — quedan {max_intentos - idx}")
else:
    print("acceso bloqueado — demasiados intentos")

print("---")

# Validar formato de datos — saltar registros inválidos
registros = ["Ana,8.5", "Bob", "Carlos,7.0", ",", "Diana,9.1"]
i = 0
while i < len(registros):
    partes = registros[i].split(",")
    i += 1
    if len(partes) != 2 or not partes[0] or not partes[1]:
        print(f"registro inválido: '{registros[i-1]}' — saltando")
        continue
    nombre, nota = partes[0], float(partes[1])
    print(f"{nombre}: {nota}")

— Patrón 2: reintentos con límite —

En análisis de datos es frecuente necesitar reintentar una operación — leer un fichero, conectar a una API, procesar un registro — hasta que funcione o hasta agotar los intentos. El while con contador de intentos es el patrón estándar.

import random
random.seed(42)   # para reproducibilidad

# Simular una operación que falla aleatoriamente
def operacion_inestable():
    return random.random() > 0.6   # 40% de éxito

max_reintentos = 5
intento = 0

while intento < max_reintentos:
    intento += 1
    print(f"intento {intento}...")
    if operacion_inestable():
        print(f"éxito en el intento {intento}")
        break
    print("falló — reintentando")
else:
    print(f"operación fallida tras {max_reintentos} intentos")

— Patrón 3: acumulación con condición de parada —

# Acumular ventas hasta superar un objetivo
ventas_diarias = [120, 85, 200, 95, 150, 180, 60]
objetivo = 500
acumulado = 0
dias = 0

while acumulado < objetivo and dias < len(ventas_diarias):
    acumulado += ventas_diarias[dias]
    dias += 1
    print(f"día {dias}: +{ventas_diarias[dias-1]} → acumulado: {acumulado}")

if acumulado >= objetivo:
    print(f"\nobjetivo alcanzado en {dias} días")
else:
    print(f"\nobjetivo no alcanzado — acumulado: {acumulado}")

print("---")

# Procesar hasta encontrar primer error
datos = [10, 25, -3, 18, 40]
i = 0
suma = 0

while i < len(datos):
    valor = datos[i]
    if valor < 0:
        print(f"error en posición {i}: valor negativo ({valor})")
        break
    suma += valor
    i += 1
else:
    print(f"todos válidos — suma: {suma}")

— for vs while: cómo los usa la IA —

La IA tiende a usar for siempre que puede — es más seguro porque no puede producir bucles infinitos. Usa while cuando el problema lo requiere explícitamente: reintentos, validación interactiva, condiciones de parada que dependen de datos externos.

Cuando veas un while en código generado por IA, verifica siempre dos cosas: que la condición cambia dentro del bucle y que hay un límite de iteraciones o un break garantizado. Un while True sin break garantizado es casi siempre un error.

  1. Predice qué imprime este código antes de ejecutarlo:
    i = 0
    while i < 8:
        i += 1
        if i % 3 == 0:
            continue
        if i > 6:
            break
        print(i, end=" ")
  2. En el patrón de validación con contraseña: ¿cuándo se ejecuta el else del while? ¿Qué imprimiría si la contraseña correcta fuera "clave_incorrecta" — algo que no aparece en la lista de intentos?
  3. Este código acumula valores pero tiene un error sutil. Identifícalo y corrígelo:
    valores = [5, 10, 15, 20]
    total = 0
    i = 0
    while i < len(valores):
        total += valores[i]
        print(f"acumulado: {total}")
    print(f"total: {total}")
  4. Dado este fragmento de código generado por IA que procesa un stream de datos, ¿hay algún riesgo de bucle infinito? ¿Qué añadirías para hacerlo seguro?
    datos = [3, 7, 2, 9, 1, 5]
    i = 0
    while True:
        valor = datos[i]
        print(f"procesando: {valor}")
        i += 1
        if valor > 8:
            break
Respuesta orientativa
a

Imprime: 1 2 4 5. i = 3: múltiplo de 3 → continue — no imprime. i = 6: múltiplo de 3 → continue — no imprime. i = 7: 7 > 6 → break — sale del bucle. Los valores 3, 6 y 7 no se imprimen.

b

El else del while se ejecuta cuando el bucle termina de forma natural — sin break. Si la contraseña correcta fuera "clave_incorrecta", ningún intento coincidiría, el bucle recorrería los tres intentos sin ejecutar el break y el else imprimiría "acceso bloqueado — demasiados intentos".

c

Falta incrementar i dentro del bucle — es un bucle infinito. Siempre suma valores[0] porque i nunca cambia. La corrección:

valores = [5, 10, 15, 20]
total = 0
i = 0
while i < len(valores):
    total += valores[i]
    print(f"acumulado: {total}")
    i += 1   # ← faltaba esta línea
print(f"total: {total}")
El error más frecuente con while: olvidar actualizar la variable de control. El resultado es un bucle infinito que no produce ningún mensaje de error — el programa simplemente se queda colgado.
d

Sí hay riesgo: si ningún valor de datos es mayor que 8, el bucle nunca ejecuta el break. Peor aún: cuando i supera el índice válido de la lista, produce un IndexError — no un bucle infinito, pero sí un error no controlado. La versión segura:

datos = [3, 7, 2, 9, 1, 5]
i = 0
while i < len(datos):   # guardia contra IndexError
    valor = datos[i]
    print(f"procesando: {valor}")
    i += 1
    if valor > 8:
        break
else:
    print("ningún valor superó el umbral")
Regla de oro para while True: siempre verifica que hay un break garantizado — que el código siempre alcanzará el break en alguna iteración. Si no puedes garantizarlo, añade una condición de límite en el while.
Parte IV

Bucles sobre estructuras de datos

Iterar sobre diccionarios y bucles anidados práctica

Los diccionarios tienen tres formas de iteración según qué quieras recorrer: las claves, los valores o los pares clave-valor. La forma más frecuente en código real es .items() — devuelve tuplas (clave, valor) que se pueden desempaquetar directamente en el for.

— Las tres formas de iterar sobre un diccionario —

edades = {"Ana": 25, "Bob": 30, "Carlos": 22}

# for sobre dict — por defecto itera sobre claves
for nombre in edades:
    print(nombre, end=" ")   # Ana Bob Carlos
print()

# .keys() — explícito, igual que el anterior
for nombre in edades.keys():
    print(nombre, end=" ")
print()

# .values() — solo los valores
for edad in edades.values():
    print(edad, end=" ")    # 25 30 22
print()

# .items() — pares (clave, valor) con unpacking
for nombre, edad in edades.items():
    print(f"{nombre}: {edad} años")

print("---")

# Filtrar durante la iteración
for nombre, edad in edades.items():
    if edad >= 25:
        print(f"{nombre} tiene {edad} años — mayor de 25")

.items() es la forma más útil porque da acceso a clave y valor a la vez sin necesidad de acceder al dict por clave dentro del bucle. El unpacking de la tupla (nombre, edad) conecta directamente con lo visto en la Clase 3 — es el mismo mecanismo.

— Patrones de acumulación sobre diccionarios —

ventas = {
    "manzana": 150,
    "pera":    80,
    "naranja": 120,
    "kiwi":    200,
    "fresa":   95
}

# Suma total
total = 0
for cantidad in ventas.values():
    total += cantidad
print(f"total vendido: {total}")   # equivale a sum(ventas.values())

# Producto más vendido
mejor_producto = None
mejor_cantidad = 0
for producto, cantidad in ventas.items():
    if cantidad > mejor_cantidad:
        mejor_cantidad = cantidad
        mejor_producto = producto
print(f"más vendido: {mejor_producto} ({mejor_cantidad})")

# Construir un nuevo dict con transformación
ventas_dobles = {}
for producto, cantidad in ventas.items():
    ventas_dobles[producto] = cantidad * 2
print(ventas_dobles)

— Bucles anidados: listas de listas y matrices —

Un bucle anidado es un for dentro de otro for. El bucle interior se ejecuta completo para cada iteración del bucle exterior. Si el exterior tiene n iteraciones y el interior tiene m, el bloque más interno se ejecuta n × m veces.

El caso más frecuente es recorrer una lista de listas — matrices, tablas de datos, registros anidados.

# Recorrer una matriz fila a fila
matriz = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

for fila in matriz:
    for elemento in fila:
        print(elemento, end=" ")
    print()   # salto de línea al terminar cada fila

print("---")

# Con enumerate para saber fila y columna
for i, fila in enumerate(matriz):
    for j, valor in enumerate(fila):
        print(f"[{i}][{j}]={valor}", end=" ")
    print()

print("---")

# Tabla de multiplicar — bucle anidado clásico
for i in range(1, 4):
    for j in range(1, 4):
        print(f"{i}×{j}={i*j:2}", end="  ")
    print()

print("---")

# Buscar en una tabla de ventas por producto y mes
ventas_tabla = [
    ["manzana", "ene", 150],
    ["pera",    "ene", 80],
    ["manzana", "feb", 200],
    ["pera",    "feb", 95],
]
buscar = "manzana"
for fila in ventas_tabla:
    if fila[0] == buscar:
        print(f"{fila[1]}: {fila[2]} unidades")
El coste de los bucles anidados: tres niveles de anidamiento sobre colecciones grandes puede ser muy lento. Si el bucle exterior tiene 1000 elementos y el interior también, el bloque más interno se ejecuta 1.000.000 de veces. En análisis de datos con DataFrames grandes, los bucles anidados son casi siempre la forma más lenta — pandas tiene operaciones vectorizadas que hacen lo mismo en una fracción del tiempo.
  1. Predice qué imprime este código antes de ejecutarlo:
    precios = {"café": 2.50, "té": 1.80, "zumo": 3.20}
    for bebida, precio in precios.items():
        if precio > 2.0:
            print(f"{bebida}: {precio:.2f}€")
  2. ¿Cuántas veces se ejecuta el bloque más interno en este código? Predice el resultado antes de ejecutar:
    total = 0
    for i in range(3):
        for j in range(4):
            total += 1
    print(total)
  3. Dado este diccionario de alumnos con sus notas, escribe un bucle que calcule la media de cada alumno y muestre quién tiene la nota media más alta:
    notas = {
        "Ana":    [8.5, 7.0, 9.0],
        "Bob":    [6.5, 7.5, 6.0],
        "Carlos": [9.5, 8.0, 9.0],
    }
  4. Este código recorre una matriz buscando el valor máximo. ¿Es correcto? ¿Hay alguna forma más concisa de hacerlo?
    matriz = [[3, 7, 2], [9, 1, 5], [4, 8, 6]]
    maximo = matriz[0][0]
    for fila in matriz:
        for valor in fila:
            if valor > maximo:
                maximo = valor
    print(f"máximo: {maximo}")
Respuesta orientativa
a

Imprime: café: 2.50€ y zumo: 3.20€. El té tiene precio 1.80 que no supera 2.0 — se salta.

b

El bloque más interno se ejecuta 3 × 4 = 12 veces. Imprime 12. Para cada valor de i (0, 1, 2), el bucle interior recorre los 4 valores de j (0, 1, 2, 3) — sumando 1 en cada iteración.

c
notas = {
    "Ana":    [8.5, 7.0, 9.0],
    "Bob":    [6.5, 7.5, 6.0],
    "Carlos": [9.5, 8.0, 9.0],
}

mejor = None
mejor_media = 0

for alumno, lista_notas in notas.items():
    media = sum(lista_notas) / len(lista_notas)
    print(f"{alumno}: {media:.2f}")
    if media > mejor_media:
        mejor_media = media
        mejor = alumno

print(f"\nmejor media: {mejor} ({mejor_media:.2f})")

Carlos tiene la nota media más alta: 8.83. Ana tiene 8.17 y Bob 6.67.

d

El código es correcto — recorre todos los elementos y actualiza el máximo cuando encuentra uno mayor. El resultado es 9. La forma más concisa:

# Con max() y una lista aplanada
maximo = max(max(fila) for fila in matriz)
print(f"máximo: {maximo}")

# O aplanando primero
todos = [v for fila in matriz for v in fila]
print(f"máximo: {max(todos)}")
El bucle anidado manual es correcto pero innecesario cuando Python ya tiene funciones como max() que operan sobre iterables. En la Parte V veremos las comprehensions que permiten expresar esto de forma aún más compacta.
Bucles en análisis de datos con pandas práctica

En la Clase 4 vimos que pandas tiene su propio mecanismo de filtrado booleano — el equivalente vectorizado del if. Del mismo modo, pandas tiene formas de iterar sobre filas y columnas que son más eficientes que un for explícito.

Aun así, hay situaciones donde el for sobre un DataFrame es la solución más clara — especialmente cuando la lógica es compleja o cuando trabajas con DataFrames pequeños. Conocer cuándo usarlo y cuándo no es parte de leer código generado por IA.

— Iterar sobre columnas de un DataFrame —

import pandas as pd

df = pd.DataFrame({
    "producto":  ["manzana", "pera", "naranja", "kiwi"],
    "ventas":    [150, 80, 120, 200],
    "precio":    [1.20, 0.90, 0.80, 1.50],
    "categoria": ["fruta", "fruta", "cítrico", "exótico"]
})

# for sobre un DataFrame — itera sobre nombres de columna
for col in df:
    print(f"columna: {col} — tipo: {df[col].dtype}")

print("---")

# Iterar sobre columnas con sus valores
for col in df.columns:
    if df[col].dtype == "int64" or df[col].dtype == "float64":
        print(f"{col}: media = {df[col].mean():.2f}")

— iterrows(): iterar sobre filas —

df.iterrows() devuelve pares (índice, fila) donde cada fila es una Serie con los valores de esa fila. Es el equivalente de .items() de un diccionario pero para filas de un DataFrame.

Conecta directamente con zip() y enumerate() — el unpacking de la tupla (índice, fila) es el mismo mecanismo.

# iterrows — índice y fila como Serie
for idx, fila in df.iterrows():
    print(f"[{idx}] {fila['producto']}: {fila['ventas']} unidades")

print("---")

# Lógica condicional por fila
for idx, fila in df.iterrows():
    ingreso = fila["ventas"] * fila["precio"]
    etiqueta = "alto" if ingreso > 150 else "bajo"
    print(f"{fila['producto']:10} ingreso: {ingreso:.2f}€ → {etiqueta}")

print("---")

# Acumular resultados fila a fila
total_ingresos = 0
productos_altos = []

for idx, fila in df.iterrows():
    ingreso = fila["ventas"] * fila["precio"]
    total_ingresos += ingreso
    if ingreso > 150:
        productos_altos.append(fila["producto"])

print(f"ingreso total: {total_ingresos:.2f}€")
print(f"productos alto rendimiento: {productos_altos}")

— iterrows() vs operaciones vectorizadas —

iterrows() es lento comparado con las operaciones vectorizadas de pandas. Para DataFrames grandes — miles o millones de filas — la diferencia puede ser de segundos a horas. Sin embargo, hay situaciones donde iterrows() es la opción más clara:

Úsalo cuando la lógica por fila es compleja y difícil de vectorizar, cuando el DataFrame es pequeño y la claridad importa más que la velocidad, o cuando estás depurando y quieres ver fila a fila. Evítalo cuando puedes usar filtrado booleano, apply() o np.where().

import numpy as np

# ❌ Con iterrows — lento para DataFrames grandes
ingresos = []
for idx, fila in df.iterrows():
    ingresos.append(fila["ventas"] * fila["precio"])
df["ingreso_loop"] = ingresos

# ✓ Vectorizado — rápido y conciso
df["ingreso_vec"] = df["ventas"] * df["precio"]

# Verificar que producen el mismo resultado
print(df[["producto", "ingreso_loop", "ingreso_vec"]])
print(f"\nresultados iguales: {(df['ingreso_loop'] == df['ingreso_vec']).all()}")

print("---")

# ❌ Etiqueta con iterrows
etiquetas = []
for idx, fila in df.iterrows():
    etiquetas.append("alto" if fila["ventas"] > 100 else "bajo")
df["etiqueta_loop"] = etiquetas

# ✓ Con np.where — vectorizado
df["etiqueta_vec"] = np.where(df["ventas"] > 100, "alto", "bajo")
print(df[["producto", "ventas", "etiqueta_loop", "etiqueta_vec"]])
  1. ¿Qué devuelve un for directamente sobre un DataFrame — sin .iterrows()? ¿Y iterrows()? ¿Qué tipo tiene cada elemento en cada caso?
  2. Dado el DataFrame del ejemplo, usa iterrows() para imprimir solo los productos de categoría "fruta" con sus ventas y precio.
  3. ¿Cuándo usarías iterrows() y cuándo la operación vectorizada? Clasifica estos casos:
    — Calcular el precio con IVA (precio × 1.21) para cada producto
    — Aplicar una lógica compleja que depende de tres columnas con muchas condiciones anidadas
    — Filtrar filas con ventas mayores de 100
    — Imprimir un resumen legible fila a fila para depurar
  4. Este código generado por IA calcula la suma de ingresos usando iterrows(). ¿Es correcto? ¿Hay una forma más eficiente de hacerlo?
    total = 0
    for idx, fila in df.iterrows():
        total += fila["ventas"] * fila["precio"]
    print(f"total: {total:.2f}€")
Respuesta orientativa
a

Un for directamente sobre un DataFrame itera sobre los nombres de columna — devuelve strings. iterrows() itera sobre las filas — devuelve tuplas (índice, Serie) donde el índice es un entero o el índice del DataFrame y la Serie contiene los valores de esa fila con los nombres de columna como índice.

b
for idx, fila in df.iterrows():
    if fila["categoria"] == "fruta":
        print(f"{fila['producto']}: {fila['ventas']} uds — {fila['precio']:.2f}€")

Imprime manzana y pera — los dos productos de categoría fruta. Naranja es cítrico y kiwi es exótico — se saltan.

c

Precio con IVA → vectorizado: df["precio"] * 1.21 — operación simple sobre toda la columna.

Lógica compleja con muchas condiciones anidadas → iterrows(): cuando la vectorización requeriría muchos np.where() anidados difíciles de leer.

Filtrar filas con ventas > 100 → vectorizado: df[df["ventas"] > 100] — filtrado booleano directo.

Imprimir resumen para depurar → iterrows(): cuando necesitas ver cada fila con formato personalizado durante el desarrollo.

d

El código es correcto pero ineficiente. La forma vectorizada es mucho más concisa y rápida:

# Vectorizado — una línea
total = (df["ventas"] * df["precio"]).sum()
print(f"total: {total:.2f}€")
Patrón de lectura: cuando veas iterrows() en código generado por IA con una operación simple dentro — suma, multiplicación, comparación — casi siempre hay una versión vectorizada más eficiente. iterrows() está justificado cuando la lógica dentro del bucle es compleja y difícil de expresar con operaciones de pandas.
Parte V

List y dict comprehensions

List comprehension: construir listas de forma concisa teoría

Una list comprehension es una forma compacta de construir una lista a partir de otra colección. En lugar de crear una lista vacía, iterar con for y hacer append en cada iteración, se expresa todo en una sola línea.

No es solo una abreviación — es una forma de comunicar intención: cuando ves una list comprehension en código ajeno sabes inmediatamente que el resultado es una lista construida a partir de una transformación de otro iterable.

— De for con append a list comprehension —

# Doblar cada número de una lista

# Versión con for y append
numeros = [1, 1, 2, 3, 5, 8, 13]
dobles = []
for n in numeros:
    dobles.append(n * 2)
print(dobles)

# List comprehension — equivalente exacto
dobles = [n * 2 for n in numeros]
print(dobles)

# Más ejemplos
palabras = ["python", "datos", "análisis"]

# Longitud de cada palabra
longitudes = [len(p) for p in palabras]
print(longitudes)

# En mayúsculas
mayusculas = [p.upper() for p in palabras]
print(mayusculas)

# Cuadrados del 1 al 10
cuadrados = [x ** 2 for x in range(1, 11)]
print(cuadrados)

# Tuplas (x, x²) — la expresión puede ser cualquier objeto
pares = [(x, x ** 2) for x in range(1, 6)]
print(pares)

— La sintaxis: cómo leerla —

La sintaxis es:

[ expresión for elemento in iterable ]

Para leerla en voz alta: "construye una lista con [expresión] para cada [elemento] en [iterable]". Siempre produce una lista nueva — el iterable original no se modifica.

# Leer en voz alta:
# "lista de n*2 para cada n en numeros"
dobles = [n * 2 for n in numeros]

# "lista de len(p) para cada p en palabras"
longitudes = [len(p) for p in palabras]

# "lista de precio*1.21 para cada precio en precios"
precios = [10.0, 25.5, 8.0, 15.0]
con_iva = [precio * 1.21 for precio in precios]
print([f"{p:.2f}" for p in con_iva])

# Con pandas — aplicar una transformación a una columna
import pandas as pd
df = pd.DataFrame({"precio": precios, "producto": ["A", "B", "C", "D"]})

# List comprehension para crear una nueva columna
df["precio_iva"] = [p * 1.21 for p in df["precio"]]
# Equivalente vectorizado — más eficiente
df["precio_iva"] = df["precio"] * 1.21
print(df)

— Cuándo usar comprehension y cuándo no —

El consejo de tus slides es el correcto: empieza con un for normal — asegúrate de entender bien la lógica — y luego conviértelo a comprehension si el resultado es más legible. Una comprehension que necesita más de una línea para leerse probablemente debería ser un for normal.

# ✓ Comprehension apropiada — simple y legible
cuadrados = [x ** 2 for x in range(10)]

# ❌ Comprehension inapropiada — demasiado densa
resultado = [x ** 2 for x in range(100) if x % 2 == 0 and x % 3 == 0 and x > 10]

# ✓ La misma lógica — más legible en varias líneas
resultado = [
    x ** 2
    for x in range(100)
    if x % 2 == 0
    and x % 3 == 0
    and x > 10
]
print(resultado)

# ❌ Comprehension para efectos secundarios — usar for normal
# [print(x) for x in lista]  ← nunca hagas esto
# Las comprehensions son para CONSTRUIR listas, no para efectos
  1. Reescribe estos bucles como list comprehension en una línea:
    # A
    resultado = []
    for x in range(1, 6):
        resultado.append(x ** 3)
    
    # B
    nombres = ["ana", "bob", "carlos"]
    mayus = []
    for n in nombres:
        mayus.append(n.capitalize())
    
    # C
    ventas = [150, 80, 200, 95]
    ingresos = []
    for v in ventas:
        ingresos.append(v * 1.20)
  2. Predice qué produce cada comprehension antes de ejecutar:
    print([x * 2 for x in range(5)])
    print([c.upper() for c in "hola"])
    print([len(p) for p in ["uno", "dos", "tres"]])
    print([(x, x**2) for x in range(1, 4)])
  3. ¿Por qué no deberías usar una list comprehension para imprimir elementos? ¿Qué devuelve [print(x) for x in [1,2,3]]?
  4. Este código generado por IA usa una list comprehension. ¿Es legible? ¿Lo reescribirías? ¿Cómo?
    datos = [3, 7, 1, 9, 4, 6, 2, 8]
    resultado = [x**2 if x%2==0 else x**3 for x in datos if x>3]
    print(resultado)
Respuesta orientativa
a
# A
resultado = [x ** 3 for x in range(1, 6)]

# B
mayus = [n.capitalize() for n in nombres]

# C
ingresos = [v * 1.20 for v in ventas]
b

[x * 2 for x in range(5)] → [0, 2, 4, 6, 8]. [c.upper() for c in "hola"] → ['H', 'O', 'L', 'A']. [len(p) for p in ["uno", "dos", "tres"]] → [3, 3, 4]. [(x, x**2) for x in range(1, 4)] → [(1, 1), (2, 4), (3, 9)].

c

No deberías usar una comprehension para imprimir porque las comprehensions son para construir listas, no para efectos secundarios. [print(x) for x in [1,2,3]] imprime los valores — eso es correcto — pero devuelve [None, None, None] porque print() devuelve None. La lista resultante no tiene ninguna utilidad y confunde al lector. Para efectos secundarios usa siempre un for normal.

d

El código es correcto pero difícil de leer — combina un filtro if y un ternario if/else en una sola línea densa. La versión más legible en varias líneas:

resultado = [
    x**2 if x % 2 == 0 else x**3
    for x in datos
    if x > 3
]
print(resultado)
# filtra x > 3: [7, 9, 4, 6, 8]
# pares → cuadrado, impares → cubo
# [343, 729, 16, 36, 64]
Regla de lectura: cuando una comprehension tiene más de dos elementos — expresión, for y if — rómpela en varias líneas. La legibilidad es más importante que la compacidad.
List comprehension con if y comprehensions anidadas práctica

Las list comprehensions admiten dos formas de condicional: el if al final para filtrar elementos, y el if/else en la expresión para transformar elementos según una condición. Son dos patrones distintos con sintaxis distintas — confundirlos es uno de los errores más frecuentes.

— if al final: filtrar elementos —

El if al final de una comprehension filtra el iterable — solo incluye los elementos que cumplen la condición. La sintaxis es: [expresión for elemento in iterable if condición].

# Extraer solo los números positivos
nums = [4, -1, 9, 0, -4, 7, -2]

# Versión con for
positivos = []
for n in nums:
    if n > 0:
        positivos.append(n)

# List comprehension con filtro
positivos = [n for n in nums if n > 0]
print(positivos)   # [4, 9, 7]

# Filtrar palabras largas
palabras = ["python", "es", "genial", "para", "programar"]
largas = [p for p in palabras if len(p) > 4]
print(largas)

# Filtrar y transformar a la vez
cuadrados_pares = [n**2 for n in range(10) if n % 2 == 0]
print(cuadrados_pares)   # cuadrados de 0,2,4,6,8

# Con condición más compleja
from math import sqrt
perfectos = [n**3 for n in range(101) if sqrt(n).is_integer()]
print(perfectos)

— if/else en la expresión: transformar según condición —

El if/else en la expresión — antes del for — no filtra: transforma cada elemento eligiendo entre dos valores según la condición. Todos los elementos del iterable aparecen en el resultado. La sintaxis es: [valor_true if condición else valor_false for elemento in iterable].

nums = [1, 2, 3, 4, 5]

# Etiquetar pares e impares — todos los elementos aparecen
# Versión con for
etiquetas = []
for n in nums:
    if n % 2 == 0:
        etiquetas.append("par")
    else:
        etiquetas.append("impar")

# List comprehension con if/else en la expresión
etiquetas = ["par" if n % 2 == 0 else "impar" for n in nums]
print(etiquetas)

# Etiquetar ventas altas y bajas
ventas = [150, 80, 200, 30, 120]
rendimiento = ["alto" if v > 100 else "bajo" for v in ventas]
print(rendimiento)

# Aplicar descuento solo a los caros
precios = [10.0, 25.0, 8.0, 50.0, 15.0]
ajustados = [p * 0.9 if p > 20 else p for p in precios]
print([f"{p:.2f}" for p in ajustados])

if al final — filtra

[n for n in nums if n > 0]

Algunos elementos no aparecen.
La lista resultado puede ser
más corta que el original.

if/else en expresión — transforma

["pos" if n>0 else "neg" for n in nums]

Todos los elementos aparecen.
La lista resultado tiene siempre
la misma longitud que el original.

— Comprehensions anidadas: aplanar y construir matrices —

Una comprehension anidada tiene dos for — el primero es el bucle exterior, el segundo el interior. El orden de lectura es el mismo que en los bucles anidados: de izquierda a derecha. Son potentes pero fáciles de malinterpretar — úsalas con moderación y rómpelas en varias líneas si son complejas.

# Aplanar una lista de listas — patrón muy frecuente
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Con bucles anidados
plana = []
for fila in matriz:
    for elem in fila:
        plana.append(elem)
print(plana)

# Con comprehension anidada
plana = [elem for fila in matriz for elem in fila]
print(plana)

# Tabla de multiplicar — construir matriz
# Con bucles anidados
tabla = []
for i in range(1, 7):
    fila = []
    for j in range(1, 5):
        fila.append(i * j)
    tabla.append(fila)

# Con comprehension anidada — invertir el orden
tabla = [[i * j for j in range(1, 5)]
                  for i in range(1, 7)]
for fila in tabla:
    print(fila)
  1. ¿Cuál es la diferencia entre estas dos comprehensions? Predice el resultado de cada una antes de ejecutar:
    nums = [1, 2, 3, 4, 5, 6]
    a = [n for n in nums if n % 2 == 0]
    b = [n if n % 2 == 0 else 0 for n in nums]
    print(len(a), a)
    print(len(b), b)
  2. Reescribe este bucle como list comprehension con filtro:
    productos = [
        {"nombre": "manzana", "stock": 50},
        {"nombre": "pera",    "stock": 0},
        {"nombre": "kiwi",    "stock": 30},
        {"nombre": "fresa",   "stock": 0},
    ]
    disponibles = []
    for p in productos:
        if p["stock"] > 0:
            disponibles.append(p["nombre"])
  3. Predice qué produce esta comprehension anidada antes de ejecutar:
    resultado = [x * y
                 for x in range(1, 4)
                 for y in range(1, 4)
                 if x != y]
    print(resultado)
  4. Este código generado por IA procesa una lista de temperaturas. ¿Es correcto? ¿Es legible? ¿Cómo lo mejorarías?
    temps = [36.5, 38.2, 37.1, 39.5, 36.8]
    resultado = ["fiebre alta" if t > 39 else "fiebre" if t > 37.5 else "normal" for t in temps]
    print(resultado)
Respuesta orientativa
a

a filtra — solo los pares: longitud 3, [2, 4, 6]. b transforma — pares quedan igual, impares se convierten en 0: longitud 6, [0, 2, 0, 4, 0, 6].

La diferencia visible: a tiene menos elementos que el original. b tiene exactamente los mismos — solo cambian algunos valores. Si la longitud del resultado debe coincidir con el original, usa if/else en la expresión. Si quieres eliminar elementos, usa if al final.
b
disponibles = [p["nombre"] for p in productos
               if p["stock"] > 0]
print(disponibles)   # ['manzana', 'kiwi']

La comprehension es más concisa y comunica claramente la intención: "nombres de los productos con stock positivo".

c

Para cada par (x, y) con x, y ∈ {1, 2, 3} donde x ≠ y: (1,2)=2, (1,3)=3, (2,1)=2, (2,3)=6, (3,1)=3, (3,2)=6. Resultado: [2, 3, 2, 6, 3, 6]. Se excluyen los casos donde x == y: (1,1), (2,2), (3,3).

d

El código es correcto — produce ['normal', 'fiebre', 'normal', 'fiebre alta', 'normal']. Pero no es legible — el ternario anidado en una sola línea larga requiere esfuerzo para seguirlo. La versión más legible:

resultado = [
    "fiebre alta" if t > 39
    else "fiebre"   if t > 37.5
    else "normal"
    for t in temps
]
print(resultado)
Patrón frecuente en IA: la IA tiende a escribir ternarios anidados en una línea. Son correctos pero difíciles de leer y verificar. Siempre que veas un ternario anidado en una comprehension, rómpelo en varias líneas antes de verificar su lógica.
Dict comprehension: construir diccionarios de forma concisa práctica

La dict comprehension es el equivalente de la list comprehension pero para diccionarios. En lugar de construir una lista, construye un diccionario asociando claves a valores en una sola expresión.

La sintaxis es: {clave: valor for elemento in iterable}. Al igual que la list comprehension, admite un if al final para filtrar elementos.

— De for con dict a dict comprehension —

# Construir un dict de longitudes de palabras
palabras = ["apple", "banana", "cherry"]

# Versión con for
longitudes = {}
for palabra in palabras:
    longitudes[palabra] = len(palabra)
print(longitudes)

# Dict comprehension — equivalente exacto
longitudes = {palabra: len(palabra) for palabra in palabras}
print(longitudes)

# Cuadrados del 1 al 5
cuadrados = {x: x**2 for x in range(1, 6)}
print(cuadrados)

# Invertir un diccionario — claves pasan a ser valores y viceversa
original = {"a": 1, "b": 2, "c": 3}
invertido = {v: k for k, v in original.items()}
print(invertido)   # {1: 'a', 2: 'b', 3: 'c'}

— Dict comprehension con filtro —

# Solo los pares mayores de 4 — del ejemplo de tus slides
num_mascotas = {
    "gatos":    6,
    "perros":   4,
    "hamsters": 7,
    "pájaros":  3
}

# Versión con for
demasiados = {}
for mascota, num in num_mascotas.items():
    if num > 4:
        demasiados[mascota] = num
print(demasiados)

# Dict comprehension con filtro
demasiados = {m: n for m, n in num_mascotas.items() if n > 4}
print(demasiados)

# Filtrar productos con stock y calcular valor
inventario = {
    "manzana": {"precio": 1.20, "stock": 50},
    "pera":    {"precio": 0.90, "stock": 0},
    "kiwi":    {"precio": 1.50, "stock": 30},
    "fresa":   {"precio": 2.00, "stock": 0}
}

# Solo productos con stock — calcular valor total
valor_stock = {
    prod: datos["precio"] * datos["stock"]
    for prod, datos in inventario.items()
    if datos["stock"] > 0
}
print(valor_stock)

— Patrones frecuentes en análisis de datos —

import pandas as pd

df = pd.DataFrame({
    "producto": ["manzana", "pera", "naranja", "kiwi"],
    "ventas":   [150, 80, 120, 200],
    "precio":   [1.20, 0.90, 0.80, 1.50]
})

# Construir un dict producto → ventas desde un DataFrame
ventas_dict = {
    fila["producto"]: fila["ventas"]
    for _, fila in df.iterrows()
}
print(ventas_dict)

# Más eficiente — usando zip
ventas_dict = dict(zip(df["producto"], df["ventas"]))
print(ventas_dict)

# Transformar todas las columnas numéricas
medias = {
    col: df[col].mean()
    for col in df.columns
    if df[col].dtype in ["int64", "float64"]
}
print(medias)

# Frecuencia de valores — contar apariciones
categorias = ["A", "B", "A", "C", "B", "A"]
frecuencia = {c: categorias.count(c) for c in set(categorias)}
print(frecuencia)

— Resumen: list vs dict comprehension —

Tipo Sintaxis Resultado
List básica [expr for x in it] lista transformada
List con filtro [expr for x in it if cond] lista filtrada
List con ternario [a if c else b for x in it] lista transformada (misma longitud)
Dict básica {k: v for x in it} diccionario nuevo
Dict con filtro {k: v for x in it if cond} diccionario filtrado
  1. Reescribe este bucle como dict comprehension:
    ventas = [150, 80, 200, 95]
    productos = ["manzana", "pera", "naranja", "kiwi"]
    resultado = {}
    for p, v in zip(productos, ventas):
        resultado[p] = v
  2. Dado este diccionario de notas, construye con una dict comprehension un nuevo diccionario con solo los alumnos aprobados y sus notas redondeadas a un decimal:
    notas = {
        "Ana":    8.567,
        "Bob":    4.321,
        "Carlos": 7.891,
        "Diana":  3.456,
        "Eva":    6.234
    }
  3. Predice qué produce esta dict comprehension antes de ejecutar:
    texto = "mississippi"
    freq = {c: texto.count(c) for c in set(texto)}
    print(freq)
    ¿Por qué se usa set(texto) en lugar de texto directamente?
  4. Este código generado por IA invierte un diccionario. ¿Es correcto siempre? ¿Cuándo podría fallar?
    original = {"a": 1, "b": 2, "c": 1}
    invertido = {v: k for k, v in original.items()}
    print(invertido)
Respuesta orientativa
a
resultado = {p: v for p, v in zip(productos, ventas)}
print(resultado)
# {'manzana': 150, 'pera': 80, 'naranja': 200, 'kiwi': 95}

Aún más conciso sin comprehension: dict(zip(productos, ventas)) — cuando el caso es tan simple, la comprehension no añade claridad.

b
aprobados = {
    nombre: round(nota, 1)
    for nombre, nota in notas.items()
    if nota >= 5
}
print(aprobados)
# {'Ana': 8.6, 'Carlos': 7.9, 'Eva': 6.2}
c

Produce la frecuencia de cada letra única en "mississippi": {'m': 1, 'i': 4, 's': 4, 'p': 2} — aunque el orden puede variar porque viene de un set.

Se usa set(texto) en lugar de texto para iterar solo sobre las letras únicas — si iteraras sobre texto directamente calcularías texto.count(c) 11 veces en lugar de 4, repitiendo el cálculo para cada aparición de cada letra.

Patrón eficiente: {c: texto.count(c) for c in set(texto)} — iterar sobre los valores únicos para evitar cálculos repetidos. Es la forma más concisa de contar frecuencias con una dict comprehension.
d

No es correcto siempre. Con original = {"a": 1, "b": 2, "c": 1} los valores 1 aparecen dos veces — en "a" y en "c". Al invertir, la clave 1 solo puede tener un valor: el último que se procesa gana. Imprime {1: "c", 2: "b"} — la clave "a" se pierde silenciosamente.

Error silencioso: invertir un diccionario con valores duplicados pierde datos sin ningún mensaje de error. Antes de invertir verifica que los valores son únicos: len(set(d.values())) == len(d).
Parte VI

Tareas para casa

Tareas para antes de la Clase 6 tareas

Esta vez las tareas son distintas: no hay pseudocódigos ni IA. Te damos directamente tres scripts que combinan todo lo visto en la Clase 5. Tu trabajo es leerlos, entenderlos y predecir su comportamiento antes de ejecutarlos.

Para cada script: lee el código completo, responde las preguntas de predicción sin ejecutar, y luego verifica ejecutándolo en JupyterLab. Si tu predicción no coincide con el resultado, identifica en qué línea te equivocaste y por qué. Lo discutiremos al inicio de la Clase 6.

— Script 1 (obligatorio): análisis de ventas —

# Script 1: análisis de ventas
ventas = [
    {"producto": "manzana",  "cantidad": 150, "precio": 1.20},
    {"producto": "pera",      "cantidad": 80,  "precio": 0.90},
    {"producto": "naranja",   "cantidad": 120, "precio": 0.80},
    {"producto": "kiwi",      "cantidad": 200, "precio": 1.50},
    {"producto": "fresa",     "cantidad": 95,  "precio": 2.00},
]

# Bloque A
ingresos = {v["producto"]: round(v["cantidad"] * v["precio"], 2)
            for v in ventas}
print("A:", ingresos)

# Bloque B
total = 0
mejor = None
for producto, ingreso in ingresos.items():
    total += ingreso
    if mejor is None or ingreso > ingresos[mejor]:
        mejor = producto
print(f"B: total={total:.2f}, mejor={mejor}")

# Bloque C
destacados = [p for p, i in ingresos.items() if i > 150]
print("C:", destacados)

# Bloque D
etiquetas = {p: "alto" if i > 150 else "medio" if i > 80 else "bajo"
             for p, i in ingresos.items()}
print("D:", etiquetas)

# Bloque E
for pos, (prod, etiq) in enumerate(
        sorted(etiquetas.items(), key=lambda x: ingresos[x[0]], reverse=True),
        start=1):
    print(f"  {pos}. {prod:10} [{etiq}] {ingresos[prod]:.2f}€")

Antes de ejecutar, responde:

  1. ¿Qué estructura construye el Bloque A? ¿Qué claves y qué valores tiene?
  2. En el Bloque B, ¿por qué la condición es mejor is None or ... en lugar de solo ingreso > ingresos[mejor]? ¿Qué pasaría sin el mejor is None?
  3. ¿Qué productos aparecen en destacados del Bloque C?
  4. En el Bloque D, ¿qué etiqueta recibe la fresa con ingreso 190€? ¿Y la pera con 72€?
  5. ¿Qué imprime el Bloque E? ¿En qué orden aparecen los productos?

— Script 2 (obligatorio): limpieza y clasificación —

# Script 2: limpieza y clasificación de registros
registros = [
    ("Ana López",    8.5,  True),
    ("",              7.0,  True),
    ("Bob García",   11.0, True),
    ("Carlos Díaz",  6.5,  False),
    ("Diana Ruiz",   4.5,  True),
    ("Eva Martín",   7.8,  True),
]

validos = []
errores = []
i = 0

while i < len(registros):
    nombre, nota, activo = registros[i]
    i += 1

    if not nombre:
        errores.append(f"registro {i}: nombre vacío")
        continue

    if not (0 <= nota <= 10):
        errores.append(f"registro {i}: nota fuera de rango ({nota})")
        continue

    if not activo:
        errores.append(f"registro {i}: {nombre} inactivo")
        continue

    validos.append((nombre, nota))

print("Errores:")
for e in errores:
    print(f"  {e}")

print("\nVálidos:")
aprobados = [(n, nota) for n, nota in validos if nota >= 5]
suspendidos = [(n, nota) for n, nota in validos if nota < 5]

for pos, (nombre, nota) in enumerate(
        sorted(validos, key=lambda x: x[1], reverse=True),
        start=1):
    estado = "✓" if nota >= 5 else "✗"
    print(f"  {pos}. {nombre:15} {nota:.1f} {estado}")

print(f"\naprobados: {len(aprobados)}, suspendidos: {len(suspendidos)}")
media = sum(n for _, n in validos) / len(validos)
print(f"media válidos: {media:.2f}")

Antes de ejecutar, responde:

  1. ¿Cuántos registros terminan en validos y cuántos en errores? ¿Cuáles son cada uno?
  2. ¿Por qué el índice i del mensaje de error ya está incrementado cuando se añade a errores? ¿Qué número aparece para el registro con nombre vacío?
  3. ¿En qué orden se imprimen los alumnos válidos? ¿Quién aparece primero?
  4. ¿Qué hace sum(n for _, n in validos)? ¿Qué tipo de objeto es (n for _, n in validos)?

— Script 3 (opcional): procesamiento de texto —

# Script 3: análisis de frecuencias en un texto
texto = "python es genial para analizar datos con python"

# Bloque A
palabras = texto.split()
freq = {p: palabras.count(p) for p in set(palabras)}
print("A:", freq)

# Bloque B
ordenadas = sorted(freq.items(), key=lambda x: x[1], reverse=True)
print("B:", ordenadas)

# Bloque C
top3 = [p for p, _ in ordenadas[:3]]
print("C:", top3)

# Bloque D
letras_unicas = sorted(set(
    letra
    for palabra in palabras
    for letra in palabra
    if letra not in "aeiou"
))
print("D:", letras_unicas)

# Bloque E
i = 0
while i < len(ordenadas):
    palabra, cuenta = ordenadas[i]
    if cuenta == 1:
        break
    print(f"E: '{palabra}' aparece {cuenta} veces")
    i += 1
else:
    print("E: todas las palabras aparecen más de una vez")

Antes de ejecutar, responde:

  1. ¿Qué devuelve el Bloque A? ¿Cuántas claves tiene freq? ¿Cuál tiene valor 2?
  2. ¿Qué contiene top3 del Bloque C? ¿Por qué se usa _ en el unpacking?
  3. ¿Qué hace el Bloque D? ¿Por qué hay dos for en la comprehension?
  4. En el Bloque E, ¿cuándo se ejecuta el else del while? ¿Se ejecuta en este caso? ¿Qué imprime?

Lo que hay que verificar en cada script: identifica cada estructura de control — for, while, break, continue — y cada comprehension. Para cada una pregúntate: ¿cuántas iteraciones tiene? ¿Qué produce? ¿Hay algún caso límite que pueda dar un resultado inesperado? Lo discutiremos al inicio de la Clase 6.

Respuesta orientativa

— Script 1: análisis de ventas —

1

El Bloque A construye un diccionario con los productos como claves y sus ingresos como valores: {'manzana': 180.0, 'pera': 72.0, 'naranja': 96.0, 'kiwi': 300.0, 'fresa': 190.0}.

2

Sin mejor is None, la primera iteración intentaría evaluar ingresos[mejor] con mejor = None — KeyError. El mejor is None actúa como guardia — la primera iteración siempre asigna el primer producto sin necesidad de comparar.

3

destacados contiene los productos con ingreso mayor de 150: ['manzana', 'kiwi', 'fresa'] — manzana 180, kiwi 300, fresa 190. Pera (72) y naranja (96) no pasan el filtro.

4

Fresa con 190€ → "alto" porque 190 > 150. Pera con 72€ → "bajo" porque no supera 150 ni 80.

5

El Bloque E ordena los productos por ingreso descendente e imprime el ranking con posición, nombre, etiqueta e ingreso:

  1. kiwi       [alto]  300.00€
  2. fresa      [alto]  190.00€
  3. manzana    [alto]  180.00€
  4. naranja    [medio]  96.00€
  5. pera       [bajo]   72.00€

— Script 2: limpieza y clasificación —

1

Terminan en validos: Ana López (8.5), Diana Ruiz (4.5) y Eva Martín (7.8) — 3 registros. Terminan en errores: registro 2 (nombre vacío), registro 3 (nota 11 fuera de rango), registro 4 (Carlos inactivo) — 3 errores.

2

i se incrementa al inicio del bucle — antes de los continue. El registro con nombre vacío es el segundo de la lista pero cuando se añade el error, i ya vale 2. Por eso el mensaje es "registro 2: nombre vacío".

3

Se ordenan por nota descendente — Eva (7.8) primera, Ana (8.5) primera en realidad:

  1. Ana López      8.5 ✓
  2. Eva Martín     7.8 ✓
  3. Diana Ruiz     4.5 ✗
4

sum(n for _, n in validos) suma las notas de los alumnos válidos: 8.5 + 4.5 + 7.8 = 20.8. La media es 20.8 / 3 = 6.93. (n for _, n in validos) es un generador — un iterador perezoso que produce valores uno a uno sin crear una lista en memoria. Es el concepto que conecta con yield de la Clase 6.

— Script 3: procesamiento de texto —

1

El texto tiene 8 palabras pero solo 7 únicas — "python" aparece dos veces. freq tiene 7 claves. Solo "python" tiene valor 2.

2

top3 contiene las tres palabras más frecuentes: ["python", ...] — "python" primero con 2, el resto con 1 en orden arbitrario. Se usa _ para ignorar la cuenta — solo nos interesa el nombre de la palabra.

3

El Bloque D extrae todas las consonantes únicas del texto en orden alfabético. El primer for recorre las palabras, el segundo recorre las letras de cada palabra, el if filtra las vocales. El set() elimina duplicados y sorted() ordena alfabéticamente.

4

El else del while se ejecuta cuando el bucle termina sin break. En este caso la lista ordenada empieza con "python" (cuenta 2) — se imprime. El siguiente tiene cuenta 1 → break. El else no se ejecuta. Imprime solo: E: 'python' aparece 2 veces.

El generador del Script 2: (n for _, n in validos) es una generator expression — como una list comprehension pero entre paréntesis y sin construir la lista en memoria. Es el anticipo de yield que veremos en la Clase 6.