Saltar a contenido

Iterables

El protocolo Iterable se usa por tipos donde es posible procesar sus contenidos de uno en uno, en orden. Un Iterable es un objeto que va a proporcionar un Iterator que puede ser usado para realizar este proceso.

Hay muchos tipos iterables en Python incluyendo List, Sets, Dictionaries, Tuples, etc. Todos ellos son contenedores iterables que pueden proporcionar un iterador.

Para ser un tipo iterable solo es necesario implementar el método __iter__() (que es el único método en el protocolo Iterable). Este método debe proporcionar una referencia al objeto Iterator. Esta referencia puede ser al propio tipo de dato o puede ser a cualquier otro tipo que implemente el protocolo Iterator.

Iteradores

Un iterador es un objeto que devuelve una secuencia de valores. Esta secuencia puede ser finita en longitud o infinita (aunque mucho iteradores orientados a contenedores proporcionan un conjunto fijo de valores).

El protocolo Iterator especifica el método __next__(). Este método debe devolver el siguiente elemento de la secuencia o lanzar una excepción de tipo StopIteration para indicar que no existen más valores.

Como ejemplo vamos a crear una clase llamada Pares que devuelva números pares entre 0 y un determinado límite. Este nuevo tipo va a actuar simultaneamente como Iterable y como Iterator al implementar ambos protocolos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Pares(object):

    def __init__(self, limite):
        self.limite = limite
        self.valor = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.valor > self.limite:
            raise StopIteration
        else:
            valor_actual = self.valor
            self.valor += 2
            return valor_actual
Ahora podemos utilizar la clase Pares con un bucle for:

1
2
3
for numero in Pares(10):
    print(numero, end=', ')
print('Fin')

Resultado:

0, 2, 4, 6, 8, 10, Fin

Generadores

Un generador (Generator) es una función especial que se puede usar para generar una secuencia de valores para iterar sobre ellos a demanda. Para ello se utiliza la palabra clave yield. Esta palabra clave solo se puede utilizar dentro de una función o de un método.

Cuando se ejecuta yield, la ejecución de la función se suspende y se devuelve el valor que acompaña a yield. La ejecución de la función generador se activará de nuevo en la siguiente llamada a partir del punto donde se suspendió.

Veamos un ejemplo sencillo:

1
2
3
4
5
6
7
8
def genera_numeros():
    yield 1
    yield 2
    yield 3

for numero in genera_numeros():
    print(numero, end=', ')
print('Fin')

Resultado:

1, 2, 3, Fin

La función genera_numeros es una función especial que devuelve un objeto Generator que es el que genera los valores requeridos para la iteración del for.

Veamos el mismo ejemplo enriquecido para que nos aclare el orden de ejecución de las cosas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def genera_numeros():
    print('Inicio')
    yield 1
    print('Siguiente')
    yield 2
    print('Otro mas')
    yield 3
    print('Termino')

for numero in genera_numeros():
    print(numero)
print('Fin')

Resultado:

Inicio
1
Siguiente
2
Otro mas
3
Termino
Fin

Vamos a reescribir el ejemplo del iterable de Pares utilizando una función que actue como generador. Veremos que queda mucho más sencillo.

1
2
3
4
5
6
7
8
9
def pares_hasta(limite):
    valor = 0
    while valor <= limite:
        yield valor
        valor += 2

for numero in pares_hasta(10):
    print(numero, end=', ')
print('Fin')

Resultado:

0, 2, 4, 6, 8, 10, Fin

No es necesario un bucle for para trabajar con una función generador. El objeto generador que devuelve la función soporta la función next. Esta función se invoca pasando como argumento el objeto generador y devuelve el siguiente valor de la secuencia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def pares_hasta(limite):
    valor = 0
    while valor <= limite:
        yield valor
        valor += 2

pares = pares_hasta(4)
print(next(pares))
print(next(pares))
print(next(pares))

Resultado:

0
2
4

Corutinas

Las corutinas (coroutines) son muy similares a los generadores pero con una diferencia: los generadores producen datos y las corutinas los consumen. Las corutinas son funciones que mediante la palabra clave yield esperan en ese punto a recibir un dato que les es suministrado mediante la función send().

Es necesario iniciar la corutina al principio con un next o con un send(None). Esto avanza la corutina hasta el yield donde se queda esperando a recibir datos para procesarlos.

Una corutina se ejecuta indefinidamente hasta que se le manda un close(). Se puede capturar el momento en que una corutina recibe el close capturando la excepción GeneratorExit y escribiendo código para ella.

Veamos un ejemplo que examina si los textos enviados a una corutina contienen una determinada palabra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def buscar(palabra):
    print('Buscando palabra', palabra)
    try:
        while True:
            linea = (yield)
            if palabra in linea:
                print(linea)
    except GeneratorExit:
        print('Saliendo de la corutina')

print('Inicio')
# Creamos la corutina
g = buscar('Python')

# Inicializamos la corutina
next(g)

# Enviamos datos a la corutina
g.send('C# es muy versatil')
g.send('Python es muy sencillo')
g.send('C++ es muy potente')

# Cerramos la corutina
g.close()
print('Fin')

Resultado:

Inicio
Buscando palabra Python
Python es muy sencillo
Saliendo de la corutina
Fin