Clase 5 — Bucles e iteración
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]
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)
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)))
precios = [12.5, 8.0, 15.3, 9.7]
for i in range(len(precios)):
print(f"precio: {precios[i]:.2f}€")
for
con range() que calcule
la suma de todos los números
impares entre 1 y 99 inclusive.
¿Puedes hacerlo en dos líneas?
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.
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].
range(2, 8) llega
hasta el 7, no el 8.
Igual que en el slicing,
el último valor nunca se incluye.
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.
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}€")
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)
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?
enumerate()
empezando desde 1:
alumnos = ["Ana", "Bob", "Carlos", "Diana"]
for i in range(len(alumnos)):
print(f"Alumno {i+1}: {alumnos[i]}")
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]
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.
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.
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.
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.
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€
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")
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=" ")
continue
con registros: ¿qué imprime
si la lista es
[None, None, None]?
¿Y si es ["Ana"]?
¿Cuántas líneas imprime en cada caso?
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")
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")
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.
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".
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".
else del for
se puede leer como
"si no encontré nada que me hiciera salir".
Es el complemento natural del
break.
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")
break
cuando solo necesitas el primer resultado
— el bucle sigue procesando innecesariamente
y puede sobreescribir el resultado correcto
con uno posterior.
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
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
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
__iter__?
¿Qué error produce intentar
recorrer un float
con for?
¿Es el mismo error que con un
int?
"datos"
usando iter().
Llama a next()
cinco veces. ¿Qué produce
la sexta llamada?
¿Qué tipo tiene el iterador?
nums = [10, 20, 30]
it = iter(nums)
print(next(it))
for x in it:
print(x)
for x in nums:
print(x)
range?
¿Qué implica esto cuando necesitas
generar índices para iterar
sobre un DataFrame de un millón
de filas?
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__().
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.
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.
for sobre
el iterable siempre crea
un iterador nuevo desde el inicio.
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.
yield — que permite
crear tus propios iterables
perezosos.
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}")
while condición
directamente. Si está al final o en el medio,
while True con break
expresa mejor la intención.
n = 1
while n <= 32:
print(n, end=" ")
n *= 2
print("fin")
total = 0
i = 1
while i <= 10:
total += i
print(total)
for
y cuándo while
para cada caso?
Justifica tu elección:
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}")
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".
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
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.
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.
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.
i = 0
while i < 8:
i += 1
if i % 3 == 0:
continue
if i > 6:
break
print(i, end=" ")
else del while?
¿Qué imprimiría si la contraseña
correcta fuera
"clave_incorrecta"
— algo que no aparece
en la lista de intentos?
valores = [5, 10, 15, 20]
total = 0
i = 0
while i < len(valores):
total += valores[i]
print(f"acumulado: {total}")
print(f"total: {total}")
datos = [3, 7, 2, 9, 1, 5]
i = 0
while True:
valor = datos[i]
print(f"procesando: {valor}")
i += 1
if valor > 8:
break
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.
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".
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}")
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")
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.
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")
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}€")
total = 0
for i in range(3):
for j in range(4):
total += 1
print(total)
notas = {
"Ana": [8.5, 7.0, 9.0],
"Bob": [6.5, 7.5, 6.0],
"Carlos": [9.5, 8.0, 9.0],
}
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}")
Imprime:
café: 2.50€
y zumo: 3.20€.
El té tiene precio 1.80
que no supera 2.0
— se salta.
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.
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.
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)}")
max() que
operan sobre iterables.
En la Parte V veremos las
comprehensions que permiten
expresar esto de forma aún
más compacta.
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"]])
for
directamente sobre un DataFrame
— sin .iterrows()?
¿Y iterrows()?
¿Qué tipo tiene cada elemento
en cada caso?
iterrows() para
imprimir solo los productos
de categoría "fruta"
con sus ventas y precio.
iterrows()
y cuándo la operación vectorizada?
Clasifica estos casos:
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}€")
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.
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.
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.
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}€")
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.
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
# 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)
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)])
[print(x) for x in [1,2,3]]?
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)
# 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]
[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)].
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.
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]
for
y if —
rómpela en varias líneas.
La legibilidad es más importante
que la compacidad.
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)
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)
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"])
resultado = [x * y
for x in range(1, 4)
for y in range(1, 4)
if x != y]
print(resultado)
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)
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].
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.
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".
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).
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)
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 |
ventas = [150, 80, 200, 95]
productos = ["manzana", "pera", "naranja", "kiwi"]
resultado = {}
for p, v in zip(productos, ventas):
resultado[p] = v
notas = {
"Ana": 8.567,
"Bob": 4.321,
"Carlos": 7.891,
"Diana": 3.456,
"Eva": 6.234
}
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?
original = {"a": 1, "b": 2, "c": 1}
invertido = {v: k for k, v in original.items()}
print(invertido)
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.
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}
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.
{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.
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.
len(set(d.values())) == len(d).
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:
mejor is None or ...
en lugar de solo ingreso > ingresos[mejor]?
¿Qué pasaría sin el mejor is None?destacados del Bloque C?
— 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:
validos
y cuántos en errores?
¿Cuáles son cada uno?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?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:
freq?
¿Cuál tiene valor 2?top3
del Bloque C?
¿Por qué se usa _
en el unpacking?for
en la comprehension?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.
— Script 1: análisis de ventas —
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}.
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.
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.
Fresa con 190€ → "alto"
porque 190 > 150.
Pera con 72€ → "bajo"
porque no supera 150 ni 80.
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 —
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.
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".
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 ✗
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 —
El texto tiene 8 palabras
pero solo 7 únicas — "python"
aparece dos veces.
freq tiene 7 claves.
Solo "python"
tiene valor 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.
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.
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.
(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.