Bucles anidados
En Python, y en otros lenguajes de programación, los bucles anidados son bucles que se encuentran dentro de otros bucles.
Estos bucles pueden ser del mismo tipo:
for i in range(3):
for j in range(3):
print(f"i: {i}, j: {j}")
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
i: 1, j: 1
i: 1, j: 2
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
Otro ejemplo,
i = 0
while i < 3:
j = 0
while j < 3:
print(f"i: {i}, j: {j}")
j += 1
i += 1
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
i: 1, j: 1
i: 1, j: 2
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
O de diferentes tipos:
for i in range(3):
j = 0
while j < 3:
print(f"i: {i}, j: {j}")
j += 1
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
i: 1, j: 1
i: 1, j: 2
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
Otro ejemplo,
i = 0
while i < 3:
for j in range(3):
print(f"i: {i}, j: {j}")
i += 1
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
i: 1, j: 1
i: 1, j: 2
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
No te preocupes por lo crítico1 de los ejemplos, solo los presentamos aquí a modo de introducción al uso de bucles anidados (nested loops en inglés).
En los cuatro ejemplos anteriores, el bucle externo se ejecuta primero y el bucle interno se ejecuta completamente en cada iteración del bucle externo.
Como puedes observar, todos los ejemplos resultan en la misma salida, demostrando que se puede resolver un problema de diferentes maneras.
Aquí solo estamos presentando el anidamiento de dos bucles, pero puedes anidar tantos bucles como necesites para resolver un problema específico. Lo importante es que entiendas cómo funcionan los bucles anidados y cómo puedes utilizarlos para resolver problemas más complejos.
Ejemplo práctico de uso
Uno de los usos más comunes de los bucles anidados es para representar patrones.
Por ejemplo, si queremos imprimir un patrón de asteriscos como el siguiente:
Podemos utilizar un programa simple como el siguiente:
Sin embargo, esta solución no es escalable. Si queremos imprimir 100 asteriscos, tendríamos que escribir 100 líneas de código. En lugar de eso, podemos utilizar un bucle para obtener el mismo resultado, pero de manera dinámica, y no copiando y pegando tantas líneas de código como asteriscos queramos imprimir:
En este caso, el bucle
for
se ejecutará 3 veces, y en cada iteración imprimirá un asterisco. Mismo resultado, menos líneas de código, más eficiente y escalable.
¿Por qué usar variables en lugar de valores literales?
El hecho de haber usado el valor 3 en range(3)
significa que si quisiéramos cambiar el valor por otro valor distinto, el cambio lo debiéramos aplicar en un solo lugar y no estar agregando o quitando líneas de código.
De hecho, en lugar de haber utilizado un valor literal, podríamos haber utilizado una variable que contenga el valor deseado, y de esta manera, si necesitáramos cambiar la cantidad de asteriscos impresos, solo tendríamos que cambiar el valor de la variable sin modificar la sintaxis de la estructura repetitiva:
En este caso, la variable
cantidad
contiene el valor 3, y el buclefor
se ejecutará 3 veces, imprimiendo un asterisco en cada iteración.Si necesitamos cambiar la cantidad de asteriscos impresos, solo necesitamos cambiar el valor de la variable
cantidad
y el programa seguirá funcionando correctamente. Mismo resultado, código más eficiente y escalable.
Implementación de funciones con bucles
Podemos llevar esto un paso más allá y encapsular la lógica de impresión de asteriscos en una función, lo que nos permitirá escribir y reutilizar un código más dinámico en diferentes partes de nuestro programa, para resolver problemas más complejos.
¡Para recordar!
Las funciones son bloques de código que realizan una tarea específica y pueden ser reutilizados en diferentes partes de un programa.
Nos permiten crear abstracciones. Una abstracción, podemos decir, es una simplificación de una idea potencialmente más complicada. Si no te queda claro el concepto de abstracción, puedes estudiarlo nuevamente ahora haciendo clic aquí.
¿Cómo implementamos la abstracción de nuestro programa que imprime un patron de n asteriscos?
¿Cómo tomamos parte del código que hemos escrito y lo convertimos en una función que podemos llamar en cualquier parte de nuestro programa?
Simple. Vamos a refactorizar nuestro programa. Lo convertiremos de esto:
En esto:
def main():
cantidad = 3
imprimir_columna_de_asteriscos(cantidad)
def imprimir_columna_de_asteriscos(altura):
for _ in range(altura):
print("*")
main()
En este caso, hemos encapsulado la lógica de impresión de asteriscos en una función llamada
imprimir_columna_de_asteriscos()
. Posee un parámetroaltura
que indica cuántos asteriscos se deben imprimir.Luego, hemos creado una función principal llamada
main()
que inicializa la variablecantidad
con el valor 3 y llama a la funciónimprimir_columna_de_asteriscos()
con el argumentocantidad
.Puedes notar que argumento y parámetro se llaman distinto. El argumento es el valor que se le pasa a la función, y el parámetro es el nombre que se le da a la variable que recibe el valor. Podrían llamarse iguales, pero aunque se llamen distinto, el valor del argumento se asigna al parámetro de todas maneras. Recuerda que son dos posiciones de memoria diferentes e independientes.
La salida de este código será la misma que antes:
De esta manera, con la función hemos creado una abstracción de la lógica de impresión de asteriscos, lo que nos permite reutilizarla en diferentes partes de nuestro programa y hacer que nuestro código sea más legible y mantenible.
A esta altura hemos complejizado un poco el código. No hace nada más por ahora, pero nos está preparando para resolver problemas más sofisticados.
Una abstracción, múltiples soluciones
Lo bueno de escribir funciones es que nos permite cambiar, modificar, los detalles de implementación subyacentes (significa que nos permite cambiar cómo funciona algo por dentro, pero desde fuera, se usa exactamente igual), pero mientras no cambiemos el nombre de la función ni sus parámetros ni el resultado que devuelve, nadie podrá notar la diferencia.
¡La magia de la abstracción!
La abstracción nos permite cambiar la implementación interna de una función sin afectar el resto del programa.
Esto significa que puedes cambiar la implementación interna tanto como quieras si quieres mejorarla o hacer correcciones con el tiempo.
Es como si pudieras actualizar el motor de un coche sin tener que cambiar cómo se conduce.
¡Eso es lo que hace que las funciones sean tan útiles en programación!
Entonces, nuestra función:
Puede ser reescrita (refactorizada) de la siguiente manera:
En este caso, hemos cambiado la implementación interna de la función
imprimir_columna_de_asteriscos()
. Ahora, en lugar de utilizar un buclefor
para imprimir un asterisco en cada iteración, utilizamos la multiplicación de cadenas para imprimir un asterisco seguido de un salto de línea"\n"
tantas veces como lo indique el parámetroaltura
.Además, hemos agregado el argumento
end=" "
al final de la funciónprint()
para evitar que se imprima un salto de línea adicional después de imprimir los asteriscos.
Seguramente esta sintaxis te resulte más críptica y compleja de entender, dado que es producto de un enfoque más inteligente que hemos visto en el pasado. Pero si ejecutamos el programa seguiremos obteniendo el mismo resultado que antes:
Lo importante aquí es que la función imprimir_columna_de_asteriscos()
sigue funcionando exactamente igual que antes, pero la implementación interna ha cambiado.
Esto es la magia de la abstracción. Nos permite cambiar la implementación interna de una función sin afectar el resto del programa. Y esto es lo que hace que las funciones sean tan útiles en programación.
Una abstracción, diferentes resultados
Otra ventaja de las funciones es que nos permiten cambiar el resultado que devuelven sin cambiar el nombre de la función ni sus parámetros.
Nuestra función imprimir_columna_de_asteriscos()
actualmente imprime un patrón vertical de asteriscos. Pero, ¿qué pasa si queremos imprimir un patrón horizontal de asteriscos?
Veamos por un momento, como modificamos nuestro programa anterior:
def main():
cantidad = 3
imprimir_columna_de_asteriscos(cantidad)
def imprimir_columna_de_asteriscos(altura):
for _ in range(altura):
print("*")
main()
Por este otro:
def main():
cantidad = 3
imprimir_fila_de_asteriscos(cantidad)
def imprimir_fila_de_asteriscos(anchura):
for _ in range(anchura):
print("*", end="")
print()
main()
En este caso, hemos cambiado el nombre de la función de
imprimir_columna_de_asteriscos()
aimprimir_fila_de_asteriscos()
y hemos cambiado el nombre del parámetro dealtura
aanchura
.Además, hemos cambiado la implementación interna de la función para imprimir la cantidad de asteriscos necesarios en una sola fila, en lugar de imprimir un asterisco en cada fila, modificando el parámetro
end=""
al final de la funciónprint()
para evitar que se imprima un salto de línea adicional después de imprimir cada asterisco.Por último, al finalizar la impresión de la fila de asteriscos, hemos agregado una función
print()
adicional para imprimir un salto de línea y pasar a la siguiente fila.
La salida de este código será:
En este caso, la función
imprimir_fila_de_asteriscos()
imprime un patrón horizontal de asteriscos en lugar de un patrón vertical.
También podríamos haber escrito la función de la siguiente manera:
En este caso, hemos utilizado la multiplicación de cadenas para imprimir un asterisco tantas veces como lo indique el parámetro
anchura
. Este acercamiento es muy similar al que utilizamos en la funciónimprimir_columna_de_asteriscos()
anteriormente.
Si unimos todos los fragmentos de código desarrollados hasta ahora, nos encontraremos con un programa que se ve así:
def main():
cantidad = 3
imprimir_columna_de_asteriscos(cantidad)
imprimir_fila_de_asteriscos(cantidad)
def imprimir_columna_de_asteriscos(altura):
for _ in range(altura):
print("*")
def imprimir_fila_de_asteriscos(anchura):
for _ in range(anchura):
print("*", end="")
print()
main()
La salida de este código será:
En este caso, hemos utilizado dos funciones para imprimir un patrón vertical y otro horizontal de asteriscos. La función
imprimir_columna_de_asteriscos()
imprime un patrón vertical de asteriscos y la funciónimprimir_fila_de_asteriscos()
imprime un patrón horizontal de asteriscos.
Bucles anidados, al fin y al cabo
A esta altura te deberías estar preguntando cual es la finalidad de todo esto. ¿Por qué desarrollamos una función para imprimir un patrón vertical de asteriscos y otro patrón horizontal de asteriscos?
Resulta que es el paso previo para entender matrices y cómo se pueden recorrer los valores de una matriz.
Por ahora no ahondaremos en detalles de lo que son las matrices, pero sí veremos que una matriz es como una "pared" de valores desplegados en filas y columnas. Como si fueran patrones de asteriscos verticales y horizontales. Algo así:
Ya no estamos hablando de la unidimensionalidad (vertical u horizontal) si no de la bidimensionalidad (vertical y horizontal, coordenada en el plano).
Si volvemos a nuestra "pared" de asteriscos, podemos ver que es un patrón de 3x3. Una cuadrícula. Tiene una altura y un ancho. Es bidimensional.
¿Pero cómo imprimimos una cuadrícula de asteriscos? ¿Cómo imprimimos una matriz de asteriscos?
Piensa la solución y luego haz clic aquí para mi solución propuesta
Como hemos dicho, necesitamos tener la siguiente salida en pantalla:
Si queremos imprimir todas estas fila, pero también todas las columnas que posee cada fila, debemos pensar como podemos recorrer cada columna de cada fila en cuestión imprimiendo un asterisco en cada espacio.
Claramente es una tarea cíclica y la podemos representar a través de una impresora. Cuando una impresora imprime, lo hace de arriba hacia abajo, en filas, y mientras está sobra cada fila imprime de izquierda a derecha, en columnas.
Este es nuestro modelo mental para resolver nuestro problema y escribir un programa que se adecue a nuestra necesidad.
Siempre que imprimimos en pantalla, hasta ahora, lo hicimos comenzando en la parte superior y yendo hacia abajo, de izquierda a derecha. Entonces tenemos que generar nuestra salida de la misma manera.
Para resolver nuestro problema, imprimir nuestra cuadrícula de asteriscos, necesitamos implementar dos bucles anidados. Necesitamos un bucle para recorrer las filas y otro bucle para recorrer las columnas. Y eso es lo que vamos a hacer a continuación:
# Función principal
def main():
# Definir el ancho y la altura de la cuadrícula
ancho = 3
altura = 3
# Imprimir la cuadrícula de asteriscos
imprimir_cuadrado_de_asteriscos(ancho, altura)
# Función para imprimir una cuadrícula de asteriscos
def imprimir_cuadrado_de_asteriscos(ancho, altura):
# Bucle para recorrer las filas
for _ in range(altura):
# Bucle para recorrer las columnas
for _ in range(ancho):
# Imprimir un asterisco
print("*", end="")
# Imprimir un salto de línea al final de cada fila
print()
# Llamar a la función principal
main()
En este caso, hemos utilizado dos bucles anidados para imprimir un patrón de 3x3 asteriscos. El bucle externo recorre las filas y el bucle interno recorre las columnas. La función
imprimir_cuadrado_de_asteriscos()
imprime un patrón de asteriscos en forma de cuadrícula.Por cada elemento recorrido en el bucle externo, se recorren todos los elementos en el bucle interno. De esta manera, se imprime un patrón de asteriscos en forma de cuadrícula.
La salida de este código será:
En este caso, hemos utilizado dos bucles anidados para imprimir un patrón de 3x3 asteriscos.
Le pasamos a la función
imprimir_cuadrado_de_asteriscos()
dos argumentos:ancho
yaltura
, que indican cuántas columnas y filas de asteriscos se deben imprimir, respectivamente.La función
imprimir_cuadrado_de_asteriscos()
recorre las filas y las columnas utilizando dos bucles anidados (el bucle externo recorre las filas y el bucle interno recorre las columnas), e imprime un asterisco en cada espacio de la cuadrícula, de la siguiente manera: recorre todas las columnas de la primera fila imprimiendo un asterisco en cada columna, luego todas las columnas de la segunda fila haciendo lo mismo, y así sucesivamente hasta completar la cuadrícula de asteriscos.Al final de cada fila, se imprime un salto de línea para pasar a la siguiente fila.
Así, el bucle externo recorre las filas y el bucle interno recorre las columnas. La función
imprimir_cuadrado_de_asteriscos()
imprime un patrón de asteriscos en forma de cuadrícula.
Referencias
Vamos a clarificar el funcionamiento con el siguiente ejemplo:
Si retomamos el problema de la validación de números positivos, podemos ver que la solución propuesta no es la más adecuada:
n = int(input("¿Cuántas veces debe martillar la herramienta? "))
if n < 0:
n = int(input("Ups, necesito un número positivo. ¿Cuántas veces debe martillar la herramienta? "))
if n < 0:
n = int(input("Ups, necesito un número positivo. ¿Cuántas veces debe martillar la herramienta? "))
if …
⋮
En lugar de repetir la pregunta infinitamente a través de estructuras alternativas dentro de otras estructuras alternativas, podemos utilizar un bucle infinito controlado para validar el ingreso de datos.
Paradigma del "bucle infinito"
¿Qué es un bucle infinito? Es simplemente uno que dura para siempre.
Hemos visto anteriormente cómo eso puede suceder de manera accidental desde el punto de vista matemático. Y dijimos que debemos evitarlo a toda costa, así como también vimos de que manera podemos frenarlo en caso de que por un error de la lógica de nuestro código, y a pesar de nuestro esfuerzo por evitarlo, se produzca.
Pero, ¿qué pasa si queremos que un bucle dure para siempre? ¿Es posible? Sí, es posible. Y es algo que se hace deliberadamente en muchos programas.
Por ejemplo, si asumimos que una estructura repetitiva se ejecute sin necesidad de validar una cierta condición, podemos hacer que el bucle se ejecute para siempre.
En este caso, el bucle
while
se ejecutará infinitamente, ya que la condiciónTrue
siempre será verdadera. Así, la respuesta a la pregunta "verdadero" siempre es "verdadero".
Esta es una manera de inducir deliberadamente un bucle que, por defecto, durará para siempre. Y como dijimos, esto no debe hacerse a menos que sea absolutamente necesario.
Y entonces, ¿cómo nos beneficiamos de este paradigma para resolver el problema de la validación de números positivos, en nuestro ejemplo, y de la validación del ingreso de datos en general?
¿Qué es un bucle infinito controlado?
Un bucle infinito controlado es un bucle que dura para siempre, como el ejemplo anterior, pero que se detiene en algún momento específico. También se lo conoce como bucle centinela o loop controlado por condición indefinida.
En este enfoque, el ciclo se inicia con la suposición de que continuará indefinidamente a partir de la instrucción while True:
, pero se incluye una condición o lógica interna que rompe el bucle cuando se cumplen ciertos criterios.
¡Para recordar!
Este patrón es común en Python y otros lenguajes, y se utiliza especialmente cuando no se puede determinar de antemano cuántas veces debe repetirse el ciclo, hasta que se obtenga la entrada o el resultado deseado.
¿Cómo se detiene? A través de una instrucción de salida explícita.
Para entenderlo mejor, en Python funciona así:
- Primero, decimos
while True:
. Esto es como decir "Vamos a seguir preguntando hasta que obtengamos la respuesta que necesitamos". - Dentro de este bucle, pedimos la información que necesitamos.
- Luego, revisamos si la información es correcta (en nuestro ejemplo, si el número es positivo).
- Si es correcta, usamos una instrucción de salida explícita para salir del bucle. Es como decir "¡Genial! Obtuve lo que quería, puedo parar de preguntar".
- Si no es correcta, el bucle continúa, se repite y volvemos a pedir la información nuevamente.
Este método es muy útil porque nos permite seguir pidiendo información hasta que obtengamos exactamente lo que necesitamos, sin tener que escribir el mismo código una y otra vez.
Analicemos cómo podemos aplicar este enfoque al problema de la validación de números positivos.
continue y break
Podemos utilizar las instrucciones continue
y break
para controlar un bucle infinito, convertirlo en un bucle infinito controlado y resolver el problema de la validación de números positivos.
python | |
---|---|
Al ejecutar nuestro programa, el bucle while True
se ejecutará infinitamente hasta que el usuario ingrese un número positivo.
¿Cuántas veces debe martillar la herramienta? -5
Ups, necesito un número positivo.
¿Cuántas veces debe martillar la herramienta? -3
Ups, necesito un número positivo.
¿Cuántas veces debe martillar la herramienta? 10
El operador martillará la herramienta 10 veces.
En este caso, si el usuario ingresa un número negativo, el programa imprimirá un mensaje de error y continuará con la siguiente iteración del bucle. Es decir que el flujo del programa, al ejecutar la instrucción
continue
, vuelve al principio del bucle, como la condición esTrue
, se vuelve a ingresar en el bucle y el usuario puede ingresar un nuevo número.Si el usuario ingresa un número positivo, el programa, al ejecutar la instrucción
break
, saldrá del bucle y continuará con el resto del programa.
Optimizando el código
Luego de analizar el funcionamiento de nuestro programa:
python | |
---|---|
Podemos optimizar nuestro código para que sea más legible y eficiente. Si bien es cierto que la instrucción continue
es útil para controlar el flujo del programa, permitiendo finalizar la ejecución del bloque de código repetitivo actual sin importar finalizar con todas las instrucciones, volviendo a validar la condición y, en caso afirmativo, repetir una nueva ejecución del bucle; en este caso dicho bloque de código no posee instrucciones posteriores a la estructura if n < 0:
que requieran de una finalización de ejecución abrupta para evitar su procesamiento.
Por lo tanto, podemos simplificar nuestro código eliminando la instrucción continue
y reemplazando la estructura if n < 0:
por una estructura if n > 0:
(asumimos que algún clavo se deberá clavar) que permita la ejecución de las instrucciones posteriores a la validación de la condición.
python | |
---|---|
Aquí es importante recalcar que al validar
n > 0
y ejecutar la instrucciónbreak
, el flujo del programa saldrá del bucle y continuará con el resto del programa, evitando la ejecución de las instrucciones posteriores a la estructuraif n < 0:
.Por lo tanto, la instrucción
continue
no es necesaria en este caso.Y tampoco es necesario implementar
else
puesto que si la condiciónn > 0
no se cumple, el flujo del programa continuará con la ejecución de la instrucciónprint("Ups, necesito un número positivo.")
, pasando por alto la ejecución de las instrucciones posteriores a la estructuraif n < 0:
, en este caso, la instrucciónbreak
.
¡Para recordar!
Puedes utilizar un bucle infinito controlado para validar la entrada del usuario hasta que se cumpla una condición específica.
Este enfoque es común en Python y otros lenguajes de programación cuando se necesita asegurar que la entrada del usuario cumpla ciertos criterios.
- Se utiliza un bucle
while True
para crear un bucle infinito. - Dentro del bucle, se solicita la entrada del usuario.
- Se verifica si la entrada cumple con la condición deseada.
- Si la condición se cumple, se utiliza
break
para salir del bucle. - Si la condición no se cumple, el bucle continúa y se vuelve a solicitar la entrada.
break vs. return dentro de funciones
Existe un caso particular que se puede presentar al combinar funciones con bucles. Cuando hablamos de funciones nos referimos a las funciones que nosotros podemos definir y utilizar en nuestros programas.
Es importante tener en cuenta la diferencia entre break
y return
en Python.
break
se utiliza para salir de un bucle, como en el ejemplo anterior.return
se utiliza para devolver un valor de una función y finalizar su ejecución.
En general, no se recomienda utilizar return
dentro de un bucle, ya que puede causar problemas de legibilidad y mantenimiento.
Sin embargo, si combinamos funciones con bucles, podemos utilizar return
para salir de un bucle y una función al mismo tiempo. Veamos un ejemplo.
Crearemos la función main()
que contendrá las dos tareas principales de nuestro programa; y a su vez crearemos dos funciones que contendrán la lógica de ejecución de cada una de estas tareas específicas: solicitar_cantidad_de_martillazos()
e imprimir_cantidad_de_martillazos()
:
def main():
n = solicitar_cantidad_de_martillazos()
imprimir_cantidad_de_martillazos(n)
def solicitar_cantidad_de_martillazos():
while True:
n = int(input("¿Cuántas veces debe martillar la herramienta? "))
if n > 0:
return n # Salir del bucle y devolver el valor
print("Ups, necesito un número positivo.")
def imprimir_cantidad_de_martillazos(n):
print(f"El operador martillará la herramienta {n} veces.")
main()
En este caso, la función
solicitar_cantidad_de_martillazos()
se encarga de solicitar la cantidad de martillazos al usuario y validar que sea un número positivo. Si la cantidad es positiva, la función devuelve el valor y finaliza su ejecución, sin necesidad de ejecutarbreak
dentro de la estructura del bucle.
Si bien es cierto que en este caso la instrucción return
es válida, es importante tener en cuenta que su uso dentro de un bucle puede resultar en una estructura de código menos legible y más difícil de mantener.
Por lo tanto, se recomienda utilizar break
para salir de un bucle y return
para devolver un valor de una función:
En este caso, la instrucción
break
se utiliza para salir del bucle, yreturn
se utiliza para devolver el valor de la funciónsolicitar_cantidad_de_martillazos()
.
Ventajas del paradigma
- Simplicidad: Es fácil de entender y de implementar.
- Eficiencia: Evita la repetición de código innecesario.
- Flexibilidad: Puede adaptarse a diferentes tipos de validación de entrada.
Desventajas del paradigma
- Potencial de Bucle Infinito: Si no se maneja correctamente, puede resultar en un bucle infinito que nunca termina.
- Legibilidad: Aunque es un enfoque común, algunos desarrolladores pueden encontrarlo menos legible o intuitivo.
Conclusión
El uso de un bucle infinito controlado es un paradigma común y efectivo en Python para validar la entrada del usuario hasta que se cumpla una condición específica.
Este enfoque asegura que el programa no procederá hasta que se reciba una entrada válida, mejorando la robustez y la fiabilidad del código.
-
Críptico: Que es difícil de entender o interpretar. Se aplica a situaciones donde la información proporcionada no es clara, es ambigua o requiere un conocimiento profundo y específico para ser entendida.
En este caso, se refiere a un fragmento de código que no es inmediatamente comprensible por el programador. ↩