Cuando definimos una función estándar acostumbramos a ejecutar todo el código de la función y devolver el valor calculado a través de una sentencia return, pero python nos ofrece una alternativa: la sentencia yield. En este caso, la función no se ejecuta de una sola vez, sino que devuelve un valor y permanece en un estado pausado hasta que solicitamos el siguiente valor. Este tipo de funciones se conocen como generadores o generators.

En la mayoría de aspectos una función generatriz se parece mucho a una función estándar. La diferencia principal es que cuando se compila una función generatriz, se convierte en un objeto que soporta iteración. La ventaja de esto es que en lugar de calcular una serie completa de valores, se van calculando de uno en uno a medida que se solicitan. Una característica llamada suspensión de estado.

En la mayoría de referencias sobre cómo funcionan estas funciones aparece como ejemplo la generación de la serie de Fibonacci… aquí también:

def fibonacci():
    """
    Generador de la serie de Fibonacci.
    Definición de la serie: el primer término es 1,
    el segundo término es 1, a partir de ahí cada término
    se calcula como la suma de los dos términos anteriores
    """
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

Una vez definida la función podemos ir obteniendo cada término siguiente utilizando el método next().

>>> f = fib()
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5

A la hora de crear y utilizar generadores una lectura imprescindible consiste en la documentación del módulo itertools. Por ejemplo, si queremos obtener una lista con los diez primeros términos de la sucesión:

>>> from itertools import islice
>>> list(islice(fib(10),10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

El primer ejemplo utiliza una función con un loop infinito, pero los generadores pueden tener una longitud finita. Definamos la siguiente función:

def dias_semana():
    dias = [
        'lunes', 'martes', 'miércoles', 'jueves', 
        'viernes', 'sábado', 'domingo'
    ]

    for dia in dias:
        yield dia

Podemos usar la función en el shell de python para ver cómo funciona:

>>> d = dias_semana()
>>> d.next()
'lunes'
>>> d.next()
'martes'
...
>>> d.next()
'domingo'
>>> d.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Como vemos, una vez alcanzado el último elemento, si llamamos al método next() se procude una excepción del tipo StopIteration (ver documentación).

En el último ejemplo vemos que podemos pasar parámetros a este tipo de funciones de la misma forma que una función estándar:

def fibonacci_num(nterm):
    """
    Genera `nterm` términos de la sucesión de Fibonacci.
    """
    a, b = 1, 1
    for i in range(nterm):
        yield a
        a, b = b, a + b

Si ahora queremos obtener crear una lista con los diez primeros números de Fibonacci:

>>> list(fibonacci_num(10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Aunque cualquier generador puede definirse como una función estándar: ¿por qué usar generadores?

1. Algunos conceptos tienen una definición más clara cuando se definen como generadores.

2. En lugar de definir una función que devuelve una lista de valores, es posible hacerlo con un generador que calcula cada término «en el momento» optimizando el uso de la memoria, sobre todo en conjuntos de valores muy grandes.

3. Imprescindible en el caso de series infinitas como el caso de los números de Fibonacci que hemos visto en los ejemplos.

Es posible pasar parámetros de vuelta a las funciones generatrices. Este es un concepto más avanzado que podemos usar cuando dominemos el concepto y la implementación básica de generadores, pero si queréis profundizar en el tema la sección «Yield expressions» de la documentación habla del método send().