# Sentencia for

## <mark style="color:blue;">1. ¿Qué es la sentencia</mark> <mark style="color:blue;"></mark><mark style="color:blue;">`for`</mark><mark style="color:blue;">?</mark>

Esta estructura nos permite implementar sentencias en Python que se repitan un número finito de veces. Por esta razón a este tipo de sentencias se les denomina **bucles**.

La sintaxis utilizada para definir la sentencia `for` es la siguiente:

```python
for <variable> in <iterable>:
    <sentencia(s)>
```

`<iterable>` es una colección de elementos, por ejemplo, una lista o una tupla.

`<variable>` es una variable que recibirá uno de los elementos del objeto `<iterable>` en cada una de las itreaciones.

`<sentencia(s)>` es el bloque de sentencias en Python que se ejecutará repetidamente.

```python
colores = ["verde", "azul", "rojo"]

for color in colores:
    print(color)
    
verde
azul
rojo
```

## <mark style="color:blue;">2. Objetos iterables</mark>

En Python los iterables, como su propio nombre indica, son objetos que pueden utilizarse para realizar iteraciones.

Si un objeto es iterable, podemos pasarlo como argumento a la función por defecto de Python `iter()` y nos devolverá un **iterador**.

```python
iter("String") # String
<str_iterator at 0x12027bd60>

iter([1, 2, 3, 4]) # Lista
<list_iterator at 0x1202f4190>

iter({"uno": 1, "dos":2}) # Diccionario
<dict_keyiterator at 0x1202abdd0>

iter(45) # Un numero entero no es iterable
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 iter(45)

TypeError: 'int' object is not iterable
```

Como puedes observar, los tipos de datos string, lista, tupla, diccionario, set o frozenset son objetos iterables.

### **2.1. Iteradores**

Un iterador se va a corresponder con un objeto dentro de Python que nos va a devolver valores uno a uno cuando utilicemos la función `next()` que viene implementada por defecto.

```python
lista = ["azul", "verde", "rojo"]

lista_itr = iter(lista)

type(lista_itr)
list_iterator

next(lista_itr)
'azul'

next(lista_itr)
'verde'
```

Una cosa interesante que debemos tener en cuenta es que un iterador almacena internamente el punto en el que se encuentra a la hora de devolverte los valores, de manera que cuando invocamos la función `next()` sabe que elemento devolver.

```python
next(lista_itr)
'rojo'
```

Cuando no quedan elementos para devolver, el iterador emite una excepción:

```python
next(lista_itr)

---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Cell In[16], line 1
----> 1 next(lista_itr)

StopIteration: 
```

Entendiendo este concepto de iterador, puede entenderse mucho mejor el concepto de bucle for, pensando en él como una estructura que automáticamente genera una iterador a partir de un elemento iterable y llama repetidamente al método next() asignando el resultado a una variable hasta que no quedan elementos.

## <mark style="color:blue;">3. Claúsula</mark> <mark style="color:blue;"></mark><mark style="color:blue;">`else`</mark> <mark style="color:blue;"></mark><mark style="color:blue;">en un bucle</mark> <mark style="color:blue;"></mark><mark style="color:blue;">`for`</mark>

Un bucle `for` también puede tener una cláusula `else`. La cláusula `else` se ejecutará cuando el iterador emita una excepción debido a que **no quedan más elementos**.

```python
for color in ["azul", "verde", "rojo"]:
    print(color)
    print("----------")
else:
    print("El bucle ya no tiene más elementos")
    
azul
----------
verde
----------
rojo
----------
El bucle ya no tiene más elementos
```

## <mark style="color:blue;">4. Range</mark>

Para terminar con esta sección sobre la sentencia `for`, me gustaría presentar una función que viene por defecto en Python que se utiliza de manera muy frecuente en combinación con los bucles, la función `range()`

```python
range(5)
range(0, 5)

type(range(5))
range

list(range(5)) # Al hacer casting a una lista nos devuelve los valores
[0, 1, 2, 3, 4]
```

Una de las cosas que podemos hacer es marcar el principio y el final de la secuencia.

```python
range(5, 20)

list(range(5,20))
[5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
```

La función `range()` también admite aplicar el concepto de stride.

```python
list(range(5,20,2)) # Saltos de 2 en 2
[5, 7, 9, 11, 13, 15, 17, 19]
```

El potencial de esta función no es utilizarla junto con la función list(), ya que consume muchos recursos computacionales, sino combinarla con estructura de control de flujo como el bucle for:

```python
for num in range(0,100,5):
    print(num)
    
0
5
10
15
20
25
30
35
40
45
50
55
60
65
70
75
80
85
90
95
```
