Control de flujo iterativo#

Introducción
Bucle while
Bucle for
Iterables
Sentencia break: salida temprana de un bucle
Bucles anidados
Listas por comprensión


Introducción#

La estructura iterativa es la otra estructura de control de flujo fundamental, conocida también por el nombre de bucles (loops).

El control de flujo iterativo permite resolver cómodamente una situación frecuente en todo tipo de algoritmos: es necesario ejecutar un conjunto de sentencias una y otra vez.

Estas sentencias se repiten:

  • mientras determinada condición lógica se cumpla o,

  • un número determinado de veces.


Bucle while#

Veamos un ejemplo que mejora el código previo de introducción de un mes válido. Ahora, se vuelve a preguntar al usuario, cuantas veces sea necesario, en el caso de que se equivoque al introducir un mes:

# Un bucle para garantizar que el mes introducido es correcto

mes = int(input("Introduzca el mes del año (entre 1 y 12): "))

while mes > 12 or mes < 1:
    print("Mes introducido incorrecto. Inténtelo de nuevo.")
    mes = int(input("Introduzca el mes del año (entre 1 y 12):"))

print(f'El mes {mes} es válido.')

Observe en el fragmento anterior la sintaxis en Python del bucle while:

  • Por un lado, la palabra reservada while que lo identifica más la condición lógica y el delimitador :

  • Por el otro lado, el cuerpo de sentencias, que debe aparecer adecuadamente sangrado

Si la condición lógica o expresión de control se evalúa como True, se ejecuta el bloque de sentencias, regresando de nuevo a la sentencia while. Así, de forma iterada, hasta que la condición lógica asociada al while se evalúe como False.

Se debe comprender que, si la condición de control del bucle se evalúa inicialmente como False, las sentencias afectadas por el bucle no serán ejecutadas ni siquiera una vez. Por tanto, en el momento en que se realiza la programación, el número de veces que se repite un bucle while ¡no puede saberse de antemano!

Así, en el ejemplo anterior puede ocurrir:

  • que no se ejecute el cuerpo del while ninguna vez, si el usuario introduce un mes correcto desde el inicio

  • que se ejecute un número arbitrariamente alto, que dependerá de la mucha o poca habilidad del usuario en seguir correctamente las instrucciones que se le dan

El alumno observador puede constatar la repetición en el código de la línea:

mes = int(input("Introduzca el mes del año (entre 1 y 12):"))

Una forma alternativa de programar el ejemplo anterior es la siguiente:

# Un bucle para garantizar que el mes introducido es correcto. Versión alternativa (I).

mes = 0 # Inicializamos la variable con un valor que 
        # hace que se evalúe a True la condición de control

while mes > 12 or mes < 1:
    mes = int(input("Introduzca el mes del año (entre 1 y 12):"))
    if mes > 12 or mes < 1:
        print("Mes introducido incorrecto. Inténtelo de nuevo")

print(f'El mes {mes} es válido.')

Para este ejemplo la ventaja en legibilidad es discutible. No en vano, ahora nos hemos visto obligados a repetir la expresión de control dentro del bucle para informar al usuario del error.

Otra alternativa es forzar la entrada al bucle con una variable booleana.

# Un bucle para garantizar que el mes introducido es correcto. Versión alternativa (II).

mes_correcto = False
while not mes_correcto:
    mes = int(input("Introduzca el mes del año (entre 1 y 12):"))
    if mes > 12 or mes < 1:
        print("Mes introducido incorrecto. Inténtelo de nuevo")
    else:
        mes_correcto = True

print(f'El mes {mes} es válido.')

Bucle controlado por un contador#

El anterior es un ejemplo de un bucle controlado por una condición lógica de carácter general. En muchas ocasiones, la condición de salida del bucle viene determinada por un contador.

# Bucle controlado por un contador que suma un determinado conjunto de reales. (Versión 1)

contador = int(input('Diga cuantos números reales quiere sumar: '))

suma = 0.0
while contador > 0:
    valor = float(input('Dame un valor real: '))
    suma += valor                                       # Equivalente a suma = suma + valor
    contador = contador - 1                             # Se podría usar: contador -= 1

print("La suma de los números introducidos es", suma)

Un ejemplo de ejecución es el siguiente:

Diga cuantos números reales quiere sumar: 3
Dame un valor real: 1.1
Dame un valor real: 2.2
Dame un valor real: 3.3
La suma de los números introducidos es 6.6

En el código anterior se debe notar:

  • Antes de entrar al bucle, ya se conoce el número de veces que éste se va a repetir.

  • Si la expresión de control es False ni siquiera se entra al bucle la primera vez.

Nota: En los depuradores de los sistemas de desarrollo y en herramientas como pythontutor se puede analizar la evolución paso a paso de los programas.

El acumulador#

Por otra parte, el ejemplo anterior implementa una de las ideas básicas más habituales en programación: el concepto de acumulador. Se quiere realizar una operación (la suma) que involucra un conjunto (no determinado en el momento de la programación) de operandos. La idea consiste en dedicar una variable acumulador (suma) para contener los resultados parciales de dicha operación.

Dicha variable acumulador ha de ser inicializada antes de entrar al bucle a un valor que sea neutro respecto a la operación seleccionada (el 0 en caso de la suma). Ya dentro del bucle, la variable acumulador es actualizada en cada iteración.

Podríamos haber programado el ejemplo anterior de otra forma, igualmente válida y legible, en la que no perdemos el valor inicial de cuantos números queremos sumar. Además, resulta fácil distinguir el caso en el cual no se introdujeron números.

# Bucle controlado por un contador que suma un determinado conjunto de reales. (Versión 2)

num_valores = int(input("Diga cuantos números reales quiere sumar: "))

suma = 0.0
contador = 0
while contador < num_valores:
    valor = float(input("Deme el valor real {} a sumar: ".format(contador)))
    suma += valor
    contador += 1

if num_valores > 0:
    print("La suma de los {} números introducidos es {}".format(num_valores, suma))
else:
    print("El usuario no introdujo ningún valor.")

Un ejemplo de ejecución es el siguiente:

Diga cuantos números reales quiere sumar: 3
Deme el valor real 0 a sumar: 1.1
Deme el valor real 1 a sumar: 2.2
Deme el valor real 2 a sumar: 3.3
La suma de los 3 números introducidos es 6.6

Bucle for#

Para el caso, muy común, de los bucles controlados por contador, Python y otros lenguajes tienen el bucle for, que resulta especialmente adaptado.

Véase el ejemplo siguiente, que muestra el producto de los enteros incluidos en determinado rango:

# Determina producto de enteros en rango

inf = int(input("Diga límite inferior del rango: "))
sup = int(input("Diga límite superior del rango: "))

producto = 1
for elem in range(inf, sup+1):
    producto *= elem                             # Se podría usar: producto = producto*elem

print(f'El producto de los enteros en el rango [{inf},{sup}] es {producto}')

Un ejemplo de ejecución es el siguiente:

Diga límite inferior del rango: 2
Diga límite superior del rango: 6
El producto de los enteros en el rango [2,6] es 720

El bucle for ... in ... utiliza en este ejemplo la secuencia range().

Así, el bucle for del ejemplo asigna de forma iterada y ordenada cada uno de los enteros del rango de que se trate a la variable que actúa como contador del bucle, elem.

En el ejemplo de ejecución, la variable elem toma iteradamente los valores 2, 3, 4, 5, 6.

Por otro lado, en cada una de las iteraciones, ejecuta las sentencias contenidas en el cuerpo del bucle.

En el anterior ejemplo se tiene una nueva implementación de la idea del acumulador, en este caso representada por la variable producto, en la que se acumula precisamente el resultado de la multiplicación de los enteros en el rango. En consonancia con su propósito, en este caso el acumulador es inicializado a 1, que es el valor neutro para la operación de multiplicación.

La sentencia for de Python tiene una sintaxis que presenta diferencias notables con respecto a la que se utiliza en C/C++ y en otros lenguajes de programación:

// Bucle for en C/C++
for (int elem = inf; elem <= sup; elem = elem + 1)
{
    producto *= elem;
}

El for de C/C++ describe de manera más explícita el proceso de inicialización del contador, la expresión lógica de control que evalúa la permanencia o no en el bucle, y el incremento (o en general modificación) del contador en cada iteración.

Ahora tenemos una nueva alternativa más compacta para realizar la suma de un determinado conjunto de números reales.

# Bucle que suma un determinado conjunto de reales. Versión con for. (Versión 3)

num_valores = int(input("Diga cuantos números reales quiere sumar: "))

suma = 0.0
for i in range(num_valores):
    valor = float(input(f'Deme el valor real {i} a sumar: '))
    suma += valor

if num_valores > 0:
    print(f'La suma de los {num_valores} números introducidos es {suma}')
else:
    print("El usuario no introdujo ningún valor.")

Iterables#

Muchos de los contenedores y, entre ellos, las secuencias, son objetos iterables, objetos capaces de ir devolviendo uno por uno, desde el primero hasta el último, los elementos que los componen.

Cualquier objeto iterable es susceptible de ser recorrido por un bucle for. Es lo que hemos hecho más arriba con el iterable range().

# Crea una lista con los valores impares de una lista de enteros
lista = [1, 3, 2, 7, 5, 4, 6]
impares = []
for x in lista:
    if x % 2:
        impares.append(x)
print(impares)
[1, 3, 7, 5]
# Crea una nueva cadena sustituyendo los caracteres en blanco de una cadena por el caracter '_'
cadena = 'Hola amigos'
nueva_cadena = ''  # Cadena vacía
for c in cadena:
    if c == ' ':
        nueva_cadena += '_'
    else:
        nueva_cadena += c

print(nueva_cadena)
Hola_amigos

En la siguiente versión del ejemplo de suma de números sobre el que venimos trabajando, primero creamos una lista con los valores introducidos por teclado. Luego realizamos la suma de sus elementos.

# Bucle que suma un determinado conjunto de reales. Versión con lista y for. (Versión 4)

num_valores = int(input("Diga cuantos números reales quiere sumar: "))
lista = []
for i in range(num_valores):
    lista.append(float(input(f'Deme el valor real {i} a sumar: ')))

suma = 0.0
for x in lista:
    suma += x

if num_valores > 0:
    print(f'La suma de los {num_valores} números introducidos es {suma}')
else:
    print("El usuario no introdujo ningún valor.")

Otra posibilidad hubiese sido crear inicialmente una lista con el tamaño final y luego ir actualizando los valores. Sería sustituir las líneas:

lista = []
for i in range(num_valores):
    lista.append(float(input(f'Deme el valor real {i} a sumar: ')))

por

lista = [0]*num_valores
for i in range(num_valores):
    lista[i] = float(input(f'Deme el valor real {i} a sumar: '))

Enumeración de iterables: enumerate()#

En ocasiones, además del valor del ítem de una secuencia, necesitamos saber su posición, su índice dentro del objeto iterable.

Preste atención a la siguiente celda. El fragmento de código crea una lista de tuplas a partir de un iterable, en este caso una lista. El primer elemento de cada tupla es la posición en el iterable del segundo elemento.

# Asociando índice y elemento de un iterable en una lista de tuplas
lista = [1, 3, 5, 7, 2, 4]
lista_tuplas = []

i = 0
for x in lista:
    lista_tuplas.append((i, x))
    i += 1

print(lista_tuplas)
[(0, 1), (1, 3), (2, 5), (3, 7), (4, 2), (5, 4)]

Para no tener que explicitar el contador i del ejemplo anterior, Python proporciona la función nativa enumerate(). Esta función devuelve un objeto que es capaz de asociar en una tupla el índice y el elemento de un objeto iterable si se la invoca con la herramienta adecuada, como por ejemplo con un bucle for.

# Asociando índice y elemento de un iterable en una lista de tuplas con enumerate() 
lista = [1, 3, 5, 7, 2, 4]
lista_tuplas = []

for tupla in enumerate(lista):
    lista_tuplas.append(tupla)

print(lista_tuplas)
[(0, 1), (1, 3), (2, 5), (3, 7), (4, 2), (5, 4)]

En realidad, el fragmento anterior podría ser aún más compacto usando el constructor list() o tuple().

# Asociando índice y elemento de un iterable en una lista de tuplas con enumerate()
lista = [1, 3, 5, 7, 2, 4]
lista_tuplas = list(enumerate(lista))

print(lista_tuplas)
[(0, 1), (1, 3), (2, 5), (3, 7), (4, 2), (5, 4)]

En los siguientes ejemplos vamos a analizar diferentes alternativas para obtener a partir de una lista de enteros:

  • una lista con sus elementos impares

  • una lista con las posiciones de los elementos impares en la lista original

La primera opción que proponemos es recurrir a un contador.

# Crea listas con los valores impares y sus posiciones de una lista de enteros (Versión 1)
lista = [1, 3, 2, 7, 5, 4, 6]
impares = []
posiciones = []
i = 0
for x in lista:
    if x % 2:
        impares.append(x)
        posiciones.append(i)
    i += 1

print(f'La lista con los impares es {impares} y sus posiciones son {posiciones}.')
La lista con los impares es [1, 3, 7, 5] y sus posiciones son [0, 1, 3, 4].

Otra posibilidad para resolver el problema es ayudarnos de las funciones len() y range().

# Crea listas con los valores impares y sus posiciones de una lista de enteros (Versión 2)
lista = [1, 3, 2, 7, 5, 4, 6]
impares = []
posiciones = []
for i in range(len(lista)):
    if lista[i] % 2:
        impares.append(lista[i])
        posiciones.append(i)

print(f'La lista con los impares es {impares} y sus posiciones son {posiciones}.')
La lista con los impares es [1, 3, 7, 5] y sus posiciones son [0, 1, 3, 4].

Una tercera posibilidad sería usar la función enumerate().

# Crea listas con los valores impares y sus posiciones de una lista de enteros (Versión 3)
lista = [1, 3, 2, 7, 5, 4, 6]
impares = []
posiciones = []
for tupla in enumerate(lista):
    if tupla[1] % 2:
        impares.append(tupla[1])
        posiciones.append(tupla[0])

print(f'La lista con los impares es {impares} y sus posiciones son {posiciones}.')
La lista con los impares es [1, 3, 7, 5] y sus posiciones son [0, 1, 3, 4].

Finalmente, proponemos la versión más pitónica, también usando enumerate(). En ella, utilizamos la propiedad de desempaquetado de una tupla (de dos elementos en este ejemplo): i, x = tupla. Evitamos así la forma verbosa de acceder a los elementos de la tupla a través de sus índices, tupla[0] y tupla[1].

# Crea listas con los valores impares y sus posiciones de una lista de enteros (Versión 4)
lista = [1, 3, 2, 7, 5, 4, 6]
impares = []
posiciones = []
for i, x in enumerate(lista):  # i, x es la tupla desempaquetada
    if x % 2:
        impares.append(x)
        posiciones.append(i)

print(f'La lista con los impares es {impares} y sus posiciones son {posiciones}.')
La lista con los impares es [1, 3, 7, 5] y sus posiciones son [0, 1, 3, 4].

Agregación de elementos de diferentes iterables: zip()#

En ocasiones, nos interesa trabajar simultáneamente con los elementos que ocupan posiciones correlativas en dos o más iterables.

Preste atención a la siguiente celda, que muestra un ejemplo visto en el tema de Secuencias. Tenemos tres tuplas correspondientes a tres alumnos y queremos agregar en una lista, una tupla con los nombres y apellidos, otra con los dni’s y otra con los nia’s.

El fragmento de código crea una lista de tuplas a partir de tres iterables, en este caso tres tuplas, alumno_1, alumno_2 y alumno_3. La primera tupla de la lista de tuplas está formada por (alumno_1[0], alumno_2[0], alumno_3[0]) y así sucesivamente.

nombre_apellidos_1 = 'Juan', 'Sierra', 'Gómez'
dni_1 = '13120714E'
nia_1 = 123434
alumno_1 = nombre_apellidos_1, dni_1, nia_1


nombre_apellidos_2 = 'Pedro', 'López', 'Roldán'
dni_2 = '73131714F'
nia_2 = 471394
alumno_2 = nombre_apellidos_2, dni_2, nia_2

nombre_apellidos_3 = 'María de las Mercedes', 'Santurce', 'Bilbao'
dni_3 = '17571924T'
nia_3 = 729219
alumno_3 = nombre_apellidos_3, dni_3, nia_3

lista = []
for i in range(len(alumno_1)):
    tupla = alumno_1[i], alumno_2[i], alumno_3[i]
    lista.append(tupla)

print(lista)
[(('Juan', 'Sierra', 'Gómez'), ('Pedro', 'López', 'Roldán'), ('María de las Mercedes', 'Santurce', 'Bilbao')), ('13120714E', '73131714F', '17571924T'), (123434, 471394, 729219)]

La función nativa zip() nos permite realizar el trabajo anterior de forma más cómoda. Esta función devuelve un objeto que es capaz de asociar en una tupla los elementos de dos o más iterables que ocupan posiciones correlativas si se la invoca con la herramienta adecuada, como por ejemplo con un bucle for o los constructores list() o tuple().

lista = []
for tupla in zip(alumno_1, alumno_2, alumno_3):
    lista.append(tupla)
    
print(lista)
[(('Juan', 'Sierra', 'Gómez'), ('Pedro', 'López', 'Roldán'), ('María de las Mercedes', 'Santurce', 'Bilbao')), ('13120714E', '73131714F', '17571924T'), (123434, 471394, 729219)]

En ejemplo anterior, hubiese sido aún más compacto usando el constructor list():

lista = list(zip(alumno_1, alumno_2, alumno_3))
    
print(lista)
[(('Juan', 'Sierra', 'Gómez'), ('Pedro', 'López', 'Roldán'), ('María de las Mercedes', 'Santurce', 'Bilbao')), ('13120714E', '73131714F', '17571924T'), (123434, 471394, 729219)]

Ejemplo de cálculo del producto escalar#

Veamos una aplicación para calcular el producto escalar de dos vectores y diferentes variantes para resolverlo. En primer lugar, una versión llamémosla clásica:

# Producto escalar (versión 1)
v1 = [1.1, 2.2, 3.3, 4.4]
v2 = [2.2, 3.3, 4.4, 5.5]

prod_esc = 0.0
for i in range(len(v1)):
    prod_esc += v1[i]*v2[i]

print(f'El producto escalar de \n {v1} \ny \n {v2} \nes \n {prod_esc}')
El producto escalar de 
 [1.1, 2.2, 3.3, 4.4] 
y 
 [2.2, 3.3, 4.4, 5.5] 
es 
 48.400000000000006

Una variante usando enumerate():

# Producto escalar usando enumerate (versión 2)
v1 = [1.1, 2.2, 3.3, 4.4]
v2 = [2.2, 3.3, 4.4, 5.5]

prod_esc = 0.0
for i, x in enumerate(v1):
    prod_esc += x*v2[i]

print(f'El producto escalar de \n {v1} \ny \n {v2} \nes \n {prod_esc}')

Finalmente, la variante más elegante y pitónica usando zip(). Nótese, de nuevo, el desempaquetado in situ de la tupla devuelta en cada iteración por la función zip().

# Producto escalar usando zip (versión 3)
v1 = [1.1, 2.2, 3.3, 4.4]
v2 = [2.2, 3.3, 4.4, 5.5]

prod_esc = 0.0
for x, y in zip(v1, v2):  # x, y es la tupla desempaquetada
    prod_esc += x*y

print(f'El producto escalar de \n {v1} \ny \n {v2} \nes \n {prod_esc}')

Sentencia break: salida temprana de un bucle#

En muchas ocasiones resulta conveniente salir de un bucle, no mediante la evaluación a False de la expresión de control, sino desde dentro del bucle utilizando un if junto con la sentencia break de salto incondicional.

En los siguientes ejemplos se muestra un problema clásico en cursos introductorios de programación. Se trata de determinar si un determinado número entero es primo o no. Una advertencia: existen formas más eficientes de realizar las variantes propuestas; deben verse como un intento inicial.

# Determina si un número entero es primo. (Versión 1)

numero = int(input('Deme un entero positivo mayor que 1: '))

es_primo = True  # Variable centinela o bandera
for divisor in range(2, numero):
    if numero % divisor == 0:
        es_primo = False
        break

if es_primo:
    print("El número {} es primo".format(numero))
else:
    print("El número {} no es primo".format(numero))

Una posible ejecución de la celda es:

Deme un entero positivo mayor que 1: 97
El número 97 es primo

Se trata básicamente de aplicar la propia definición de número primo: aquel que es divisible solo por él y por la unidad.

La estrategia entonces consiste en utilizar, mediante un bucle, todos los posibles divisores legítimos que harían que se pudiera decidir que el número no es primo. Se utiliza un bucle for por rango, y el rango elegido es \([2,numero)\). Obsérvese que el límite inferior está incluido en el rango y el superior no. Y esto es precisamente lo que se requiere.

Se debe notar que el bucle necesario tiene una especie de carácter asimétrico:

  • por una parte, para concluir que el número es primo, se debe llevar el bucle hasta su conclusión, investigando todos los posibles divisores, sin hallar ningún divisor exacto.

  • por el contrario, para concluir que el número no es primo, basta con encontrar el primer divisor exacto.

En sintonía con esta asimetría del problema, se utiliza una construcción en la cual al bucle for por rango se le asocia una variable centinela, testigo o bandera, es_primo. Si en el transcurso de las iteraciones se detectara, mediante un condicional if, que el número investigado es divisible por alguno de los divisores posibles, se sale inmediatamente del bucle, utilizando la sentencia break y activando la bandera.

Ya fuera del bucle, se interroga el valor del centinela para informar al usuario sobre el resultado encontrado.

Las variables testigo son inicializadas a un valor de reposo antes de entrar a una zona del código en la que deseamos señalar si un determinado evento se ha producido o no. Las variables centinela suelen ser booleanas pero, en ocasiones, pueden usarse enteros que tomarán diferentes valores en dependencia de las sentencias que sean ejecutadas o no. De esta manera, cuando su valor sea interrogado al final, indicarán el camino concreto que siguió el programa, permitiendo sacar las oportunas conclusiones.

Se debe comprender que la salida anticipada de un bucle constituye un recurso que puede mejorar la escritura o legibilidad del código. No obstante, siempre se puede modificar el bucle de manera que solo se pueda decidir la permanencia o salida del mismo a partir de la expresión de control.

En el ejemplo que nos ocupa:

# Determina si un número entero es primo. Versión sin salida incondicional break. (Versión 2)

numero = int(input('Deme un entero positivo mayor que 1: '))

es_primo = True
divisor = 2
while es_primo and divisor < numero:
    if numero % divisor == 0:
        es_primo = False
    divisor += 1

if es_primo:
    print("El número {} es primo".format(numero))
else:
    print("El número {} no es primo".format(numero))

La modificación ha supuesto cambiar el for por un while. Observe la implementación manual del contador divisor que actúa como divisor y que debe ser inicializado fuera del bucle. Véase que la expresión de control incluye la condición de que no se haya detectado todavía un divisor para continuar iterando.

Una versión más compacta se logra si nos damos cuenta que el propio contador divisor puede actuar como variable testigo. Si divisor == numero tras el bucle es que hemos recorrido todo el rango de valores sin encontrar un divisor.

Así:

# Determina si un número entero es primo. Versión usando while y el contador como testigo. (Versión 3)

numero = int(input('Deme un entero positivo mayor que 1: '))

divisor = 2
while numero % divisor != 0:
    divisor += 1

if divisor == numero:
    print("El número {} es primo".format(numero))
else:
    print("El número {} no es primo".format(numero))

Y ahora esta última variante en su versión con bucle for por rango:

# Determina si un número entero es primo. Versión usando for y el contador como testigo. (Versión 4)

numero = int(input('Deme un entero positivo mayor que 1: '))

for divisor in range(2, numero+1):
    if numero % divisor == 0:
        break

if divisor == numero:
    print("El número {} es primo".format(numero))
else:
    print("El número {} no es primo".format(numero))

Decidir cuál de las implementaciones es superior es cuestión de debate. Más allá de optar entre las versiones for por rango o while, a nuestro juicio, el uso explícito de la variable centinela es_primo genera un código más legible aunque menos eficiente.


Sentencia continue#

De forma similar al break, la sentencia continue provoca un salto incondicional durante la ejecución de un bucle. La sentencia continue, a diferencia del break, no abandona el bucle, sino que cuando es ejecutada provoca un salto inmediato al inicio del bucle, para procesar la siguiente iteración.

# Ejemplo de uso de la sentencia continue: no se imprimen los múltiplos de 3 (versión 1)

lista = []
limite = 25
for i in range(limite):
    if i % 3 == 0:
        continue
    lista.append(i)

print(lista)
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19, 20, 22, 23]

Una forma más lógica sin continue es la que sigue:

# Ejemplo de uso de la sentencia continue: no se imprimen los múltiplos de 3 (versión 2)

lista = []
limite = 25
for i in range(limite):
    if i % 3 != 0:
        lista.append(i)

print(lista)
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19, 20, 22, 23]

La sentencia continue es poco utilizada. En algunas circunstancias ofrece una solución más estructurada y comprensible. ¡No la usaremos durante el curso!


Bucles anidados#

Con cierta frecuencia la solución de un problema exige el uso de bucles que estén contenidos dentro de otros. Los bucles anidados pueden tener tantos niveles como se quiera.

La idea clave es: en cada iteración de un bucle externo, ocurren todas las iteraciones posibles de los bucles internos.

El siguiente ejemplo es ilustrativo de este proceso. En este caso, se tienen dos bucles anidados implementados mediante for.

for i in range(1, 11):
    for j in range(1, 11):
        print(f'{i*j:4d}', end=' ')
    print()
   1    2    3    4    5    6    7    8    9   10 
   2    4    6    8   10   12   14   16   18   20 
   3    6    9   12   15   18   21   24   27   30 
   4    8   12   16   20   24   28   32   36   40 
   5   10   15   20   25   30   35   40   45   50 
   6   12   18   24   30   36   42   48   54   60 
   7   14   21   28   35   42   49   56   63   70 
   8   16   24   32   40   48   56   64   72   80 
   9   18   27   36   45   54   63   72   81   90 
  10   20   30   40   50   60   70   80   90  100 

El bucle externo recorre todos los enteros en el intervalo \([1,10]\) y utiliza un índice o contador i y el interno recorre el mismo rango pero asignando los valores del mismo a la variable de control del bucle j. De manera que, en la primera iteración del bucle externo, cuando i tiene el valor 1, el bucle interno agota todas las posibles iteraciones, con j barriendo todos los valores del rango \([1,10]\).

Dentro del bucle interno se saca por pantalla, utilizando un formato útil para alinear correctamente las columnas de una tabla, el producto de ambos contadores. Observe que en esta función print(), el caracter a imprimir al final (normalmente el cambio de línea), es sustituido por una cadena de caracteres que representa un espacio en blanco, end=' '.

El sangrado existente en el segundo print() informa al intérprete de Python que este debe ser ejecutado, como parte del bucle externo, cada vez que el interno termina su ejecución. Su propósito es simplemente provocar un cambio de línea.

El objetivo final logrado es la salida por la consola de la tabla de multiplicar. El ejemplo utilizado en este caso puede resultar trivial, pero el concepto ilustrado tiene muchas otras aplicaciones.

El siguiente ejemplo ilustra algunos de los conceptos vistos en este tema. La celda calcula una lista con las posiciones de la primera ocurrencia de cada elemento de una lista en otra. Si el elemento no se encuentra en la otra lista, se almacena None como posición.

# Posiciones de la primera ocurrencia de los elementos de lista2 en lista1
lista1 = [1, 3, 5, 7, 9, 2, 4, 6, 8, 1]
lista2 = [1, 1, 3, 3, -3, 7, -8]
posiciones = [0]*len(lista2)
for i, x in enumerate(lista2):
    encontrado = False
    for j, y in enumerate(lista1):
        if x == y:
            encontrado = True
            break
    if encontrado:
        posiciones[i] = j
    else:
        posiciones[i] = None

print(f'''La primera ocurrencia de cada uno de los elementos de la lista
 {lista2}
se encuentra en las posiciones
 {posiciones}
de la lista
 {lista1}''')
La primera ocurrencia de cada uno de los elementos de la lista
 [1, 1, 3, 3, -3, 7, -8]
se encuentra en las posiciones
 [0, 0, 1, 1, None, 3, None]
de la lista
 [1, 3, 5, 7, 9, 2, 4, 6, 8, 1]

El tipo NoneType#

El tipo NoneType permite representar aquellos objetos que no tienen valor.

Una variable de tipo NoneType solo puede tener el valor None, que es una constante literal predefinida, igual que lo son True o False.

El ejemplo anterior muestra uno de los usos habituales de este tipo. Algunos de los elementos buscados no se encuentran en la lista. Una forma concisa de reflejarlo es con el literal None.

La forma correcta de comprobar si una variable no tiene asignada un valor es con el operador is.

# Listado de los elementos de lista2 ausentes en lista1 usando la lista de posiciones
lista_ausentes = []
for i, x in enumerate(posiciones):
    if x is None:
        lista_ausentes.append(lista2[i])
print(f'''Los valores {lista_ausentes} de la lista
 {lista2}
no se encuentran en la lista
 {lista1}.''')
Los valores [-3, -8] de la lista
 [1, 1, 3, 3, -3, 7, -8]
no se encuentran en la lista
 [1, 3, 5, 7, 9, 2, 4, 6, 8, 1].

Listas por comprensión#

Un uso habitual que hemos visto de un bucle for es transformar una lista en otra. Las listas por comprensión (comprehension list) son una forma compacta de lograr el mismo efecto en una única línea.

Supongamos un sencillo ejemplo en el que deseamos obtener una lista de enteros con los cuadrados de otra. Una solución con un bucle for sería la siguiente:

# Cuadrados de una lista (versión 1)
lista = [1, 2, 3, 4, 5]
lista_cuad = []
for x in lista:
    lista_cuad.append(x*x)
print(lista_cuad)
[1, 4, 9, 16, 25]

Este mismo resultado podríamos obtenerlo con una lista por comprensión mediante el siguiente fragmento de código:

# Cuadrados de una lista usando listas por comprensión (versión 2)
lista = [1, 2, 3, 4, 5]
lista_cuad = [x*x for x in lista]
print(lista_cuad)
[1, 4, 9, 16, 25]

Entre los corchetes [] podríamos traducir:

Crea una lista con la expresión x*x para cada x perteneciente al iterable lista

Las listas por comprensión no son sino mero azúcar sintáctico, pero su uso está muy extendido en Python. Hasta que el programador novel se acostumbra, pueden parecer una construcción sintáctica que empeora la legibilidad. Sin embargo:

  • son construcciones más compactas

  • son más rápidas que su equivalente con bucle for, porque internamente se reserva espacio para la nueva lista antes de empezar a formarla.

Veamos a continuación otro uso habitual. En este caso, deseamos formar una lista a partir de otra filtrando algunos de los elementos.

Por ejemplo, formemos una lista solo con los cuadrados de los elementos pares de otra. La solución con un bucle for sería la siguiente:

lista = [1, 2, 3, 4, 5]
lista_cuad = []
for x in lista:
    if x % 2 == 0:
        lista_cuad.append(x*x)
print(lista_cuad)
[4, 16]

Ahora, su equivalente con una lista por comprensión:

lista = [1, 2, 3, 4, 5]
lista_cuad = [x*x for x in lista if x % 2 == 0]
print(lista_cuad)
[4, 16]

Entre los corchetes [] podríamos traducir:

Crea una lista con la expresión x*x para cada x perteneciente al iterable lista que cumpla x % 2 == 0

Introducción desde el teclado de una secuencia de valores#

Para finalizar las listas por comprensión, vamos a ver otro ejemplo de uso habitual que ocurre en la introducción de datos por parte de un usuario. Imaginemos que solicitamos la introducción de una serie de valores de una lista de float’s separados por espacios en blanco o por comas.

Es evidente que la cadena introducida no es transformable directamente a una colección de float’s:

valores = float(input('Introduzca valores separados por comas: '))

Si introdujésemos la cadena '1., 2., 3., 4.' saltaría una excepción ValueError avisándonos de que no es posible convertirla a float.

Los objetos str disponen del método .split(sep=',') que crean una lista de cadenas formada por las subcadenas separadas por el caracter sep, en este caso el separador coma ','. Podría haberse solicitado separar los valores por espacios en blanco, ' ', por guiones, '-', etc.

cadena = input('Introduzca valores separados por comas: ')
lista_cadenas = cadena.split(sep=',')
print(lista_cadenas)

Ahora, si introdujésemos la cadena '1., 2., 3., 4.' obtendríamos tras aplicar .split(sep=',') la lista de subcadenas ['1.', ' 2.', ' 3.', ' 4.']. Cada una de estas subcadenas sí que es susceptible de ser transformada a float. Se muestra la versión conjunta de todo el proceso:

lista = []
for x in input('Introduzca valores separados por comas: ').split(sep=','):
    lista.append(float(x))

Se muestra ahora la versión pitónica con listas por comprensión:

lista = [float(x) for x in input('Introduzca valores separados por comas: ').split(sep=',')]

En general, en este curso no haremos un uso exhaustivo de listas por comprensión, pues preferimos en un nivel inicial que el alumno trabaje con construcciones for explícitas. Sin embargo, el alumno debe habituarse a interpretar correctamente este tipo de sentencias.