viernes, octubre 06, 2006

[Python 2.5] - Nuevas posibilidades de los generadores

De antemano, y citando a Pascal, les pido disculpas por la longitud de este comentario. No he tenido tiempo de hacerlo más corto.

Generadores e Iteradores



Python 2.5 añade la posibilidad de pasar valores a un generador. Pero antes, recordemos qué es, y para qué sirve, un generador. Los generadores están con nosotros desde Python 2.3 y, básicamente, son funciones cuyo resultado es un iterador.

¿Y qué es, y para qué sirve, un iterador, se preguntará el astuto lector?

Los iteradores son contenedores, capaces de devolver sus elementos de uno en uno. Los iteradores se pueden usar en un bucle for y en otros lugares donde se necesite una secuencia, como por ejemplo, en las funciones zip(), map() o lambda() reduce(). Son iteradores todos los tipos de secuencia (como list, str y tuple), algunos otros tipos, como dict y file, y cualquier objeto cuya clase defina los métodos __iter__() y __getitem__().

El generador, decíamos, es como una función normal, solo que retorna un iterador. La única diferencia aparente entre una función normal y un generador es que se usa la palabra reservada yield en vez de return. Las funciones generadoras suelen contener uno o más bucles que devuelven (yield) elementos al llamante. La ejecución de la función se para en la palabra clave yield (devolviendo el resultado) y se reanuda cuando se solicita el siguiente elemento llamando al método next() del iterador devuelto.

Los generadores son, por tanto, una herramienta simple y potente para crear iteradores. Cada vez que se quiere obtener un nuevo elemento, el generador reanuda la ejecución donde se quedó (recordando todos los valores de los datos y la última sentencia en ejecutarse). Este ejemplo muestra lo fácil que es crear generadores:

def delreves(data):
for indice in range(len(data)-1, -1, -1):
yield data[indice]

>>> for car in delreves('Wanda'):
... print car
...
a
d
n
a
W


Cualquier cosa que se pueda hacer con generadores se puede hacer también definiendo una clase iterador. Lo único que aportan los generadores es que su métodos __iter__() y next() se crean automáticamente. Además, como las variables locales y el estado de ejecución se guardan automáticamente entre llamadas, la función generadora es más fácil de escribir y no requiere utilizar variables de instancia como self.indice o self.datos. Cuando los generadores terminan su ejecución, hacen saltar la excepción StopIteration automáticamente. La combinación de estas características hace que la creación de iteradores sea tan sencilla como la de una función normal.

En fin, más información, y más detallada sobre estos temas, se puede consultar aquí: Iteradores y aquí: Generadores.


Nuevas posibilidades



Los generadores antes de Python 2.5 sólo producían salidas; una vez realizada la primera llamada, (momento en el cual se crea el iterador), no hay forma de añadir nueva información a la función generadora. En determinados casos, la posibilidad de alterar el comportamiento de la función de una determinada manera durante la secuencia de llamadas puede ser útil. Algunas soluciones para este problema en el momento actual incluyen utilizar variables globales (¡Aurghhh!) o pasarle a la función un objeto mutable, que pueda ser modificado por el llamador de la función.

Veamos como podremos hacerlo a partir de Python 2.5 con un ejemplo. Supongamos el siguiente generador:

def counter (maximum):
i = 0
while i < maximum:
yield i
i += 1


En este punto, debería estar claro que una llamada a counter(9), por ejemplo, devolvería consecutivamente los valores desde 0 hasta 9.

A partir de Python 2.5, yield pasa de ser una sentencia a ser una expresión; en otras palabras, yield puede retornar un valor, que puede ser utilizado o asignado a una variable.
val = (yield i)

Nota: Es recomendable rodear con paréntesis la expresión yield, siempre que se utiliza para algo, como en el ejemplo anterior en que es asignado a una variable. No son siempre necesarios, pero es más fácil añadirlos siempre que aprenderse los casos especiales en que no son necesarios (De todas formas, si se es lo suficientemente aguerrido, las reglas exactas están explicadas en el PEP 342 - Coroutines via Enhanced Generators)

Los valores son enviados al generador llamando al método send(value) del generador (Recordemos que en Python, casi todo es un objeto, incluyendo las funciones). Cuando se llama a send, se continua con la ejecución del código del generador, y la expresión yield retorna el valor especificado. Si se llama al método normal, next(), yield retorna None.

En el siguiente ejemplo, modificamos el código para permitirnos cambiar el valor del contador en medio de las sucesivas llamadas.

def counter (maximum):
i = 0
while i < maximum:
val = (yield i)
# If value provided, change counter
if val is not None:
i = val
else:
i += 1

Con lo cual podriamos hacer lo siguiente:
>>> it = counter(10)
>>> print it.next()
0
>>> print it.next()
1
>>> print it.send(8)
8
>>> print it.next()
9
>>> print it.next()
Traceback (most recent call last):
File ``t.py'', line 15, in ?
print it.next()
StopIteration

Además de send(value), hay dos nuevos métodos en los generadores:

  • throw(type, value=None, traceback=None) se puede utilizar para elevar una excepción dentro del cuerpo del generador. La expresión yield elevará la excepción en la siguiente ejecución. Podemos utilizar esta llamada y pasarle la interrupción StopIteration, para forzar el fin del generador, pero para eso, mejor usar el siguiente método.


  • close() eleva una excepción de tipo GeneratorExit que obliga a finalizar el generador. Es invocada por el recolector de basura de Python justo antes de liberar el generador. Esto significa que el código del generador obtiene una última oportunidad de ser ejecutado antes de terminar. De esta forma se garantiza que las sentencias try...finally sean ejecutadas. De esta forma se elimina una restricción semántica que se había impuesto en la versión anterior, que impedía mezclar el uso de yield con try...finally. Era necesario eliminar esta restricción para poder implementar la sentencia with, descrita en el PEP 343 - The "with" Statement.


Ejercicio para el lector



Modificar el ejemplo del generador counter() para permitirnos modificar el límite máximo, en vez del contador. Las respuestas, en los comentarios. Los que consigan terminar el ejercicio serán obsequiados con una impresión de honda satisfaccion por el trabajo bien realizado.


Más información sobre las novedades en Python 2.5

5 comentarios:

Demóstoles dijo...

Y digo yo. Todo esto está muy bonito pero ¿Por qué no te pasas a un lenguaje de verdad como el C++?

:-)

ornitorrinco enmascarado dijo...

Oye, iba a responder a la pregunta, pero me salió tal rollo que al final haré un post completo ¡Gracias por la idea!

De todas formas, te adelanto un resumen de la respuesta: Python mola más.

heimy dijo...

Esto... ¿lambda()? ¿No querrías decir sum()? :)

lambda sigue siendo parte de la sintaxis del lenguaje ;)

ornitorrinco enmascarado dijo...

Heimy, tienes razón, estaba pensando en reduce y se me fue el baifo*...

En realidad, era para ver si estaban atentos... No cuela, ¿no?

* ir o irse el baifo: dícese del que, por una razón u otra, no presta la atención debida a sus tareas, siendo que al contrario, se complace en desperdigar sus neuronas en diversas y variadas ocupaciones.

ornitorrinco enmascarado dijo...

Ya está corregido, gracias.