PEP 342: Nouvelles fonctionnalités des générateurs
Avant Python 2.5, les générateurs ne pouvaient générer des données qu'à partir d'un code fixe. Maintenant, les générateurs possèdent des méthodes supplémentaires pour permettre de changer de comportement de ceux-ci. La nouvelle méthode send() permet à l'instruction yield une valeur de retour. Deux autres nouvelles méthodes permettent de plus de faire lever une exception par yield et de terminer proprement un générateur.
Python 2.5 ajoute un moyen simple de passer des valeurs dans un générateur. Introduits avec Python 2.3, les générateurs produisent seulement des sorties; une fois que le code d'un générateur a été invoqué pour créer un itérateur, il n'y a plus de possibilité d'ajout d'information dans la fonction quand son cours reprend. Parfois, la possibilité de passer de l'information à un générateur serait utile. Les bidouilles pour solutionner ce problème incluent de faire que le code du générateur soit une variable globale et ensuite de modifier la valeur de cette variable, ou encore de passer un objet mutable que l'appelant modifie ensuite.
Pour rafraîchir les idées sur les générateurs, voici un exemple simple:
def compteur (maximum):
i = 0
while i < maximum:
yield i
i +=1
Lorsque vous appelez "compteur(10)", le résultat est un itérateur qui retourne les valeurs de 0 à 9. Lorsque l'interpréteur rencontre l'instruction yield, l'itérateur retourne la valeur fournie et suspend l'exécution de la fonction, en préservant les variables locales. L'exécution reprend lorsque la méthode next() de l'itérateur est invoquée, reprenant juste après l'instruction yield.
Avec Python 2.3, yield était une instruction et son appel ne retournait aucune valeur. Dans la version 2.5, yield devient une expression, celle-ci retourne une valeur qui peut être assignée à une variable ou utilisée directement.
val = (yield i)
Je vous recommande de toujours placer des parenthèses autour de l'expression yield lorsque vous désirez faire quelque chose de la valeur retournée, comme dans l'exemple précédent. Les parenthèses ne sont pas nécessaires au sens strict, mais il est plus facile de toujours les ajouter plutôt que d'avoir à se souvenir où elles sont obligatoires.
(Le PEP 342 explique les règles exactes, qui sont qu'une expression yield doit toujours avoir des parenthèses excepté si elle apparaît comme l'expression de plus haut niveau à la droite d'une assignation. Cela veut dire que vous pouvez écrire "val = yield i" mais que vous devez ajouter des parenthèses dès lors qu'une opération est effectuée, comme dans "val = (yield i) + 12".)
Les valeurs sont envoyées dans un générateur en utilisant sa méthode send(valeur). Le code du générateur reprend alors et l'expression yield retourne la valeur spécifiée en argument. Si c'est la méthode classique next() qui est appellée, le yield revoie None.
Voici l'exemple précédent, modifié pour permettre la modification de la valeur du compteur interne.
def compteur (maximum):Et voici un exemple de modification du générateur :
i = 0
while i < maximum:
val = (yield i)
# Si une valeur est fournie, modifier le compteur
if val is not None:
i = val
else:
i += 1
>>> it = compteur(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
Puisque yield retournera None dans la plupart des cas, vous devriez toujours vérifier cette valeur. N'utilisez pas directement la valeur de retour dans des expressions à moins d'être sûr que la méthode send() sera la seule utilisée pour reprendre l'exécution du générateur.
En plus de la méthode send(), il y a deux nouvelles méthodes disponibles avec les générateurs :
-
throw(type, value=None, traceback=None) est utilisé pour lever une exception dans le générateur. L'exception est levée par l'expression yield où l'exécution s'était arrêtée.
-
close() lève une nouvelle exception GeneratorExit dans le générateur pour terminer l'itération. A la réception de cette exception, le code du générateur doit lever GeneratorExit ou StopIteration; gérer l'exception et faire quoi que ce soit d'autre est illégal et générera une RuntimeError. close() sera aussi appelée par le garbage collector (ramasse miette) de Python lorsque le générateur devient inaccessible.
Si vous voulez réaliser du code de nettoyage lorsque GeneratorExit est levé, Je vous suggère d'utiliser un "try: ... finally:" au lieu de gérer GeneratorExit.
L'effet cumulé de ces changements est de faire évoluer les générateurs de producteur d'information à un producteur et consommateur.
Les générateurs deviennent de ce fait des coroutines, une forme généralisée de sous-routine. Les sous-routines sont lancées par un point et se terminent à un autre point (respectivement : le début de la fonction et l'instruction return), mais les coroutines peuvent être entrées, suspendues, et reprises en de nombreux points (aux instructions yield). Il sera nécessaire de trouver les modèles pour une utilisation judicieuse des coroutines en Python.
L'addition de la méthode close() à un effet secondaire qui n'est pas évident à première vue. En effet, close() est appelée quand la mémoire du générateur est récupérée par le garbage collector, cela veut dire que le code du générateur obtient une dernière chance de s'exécuter avant que le générateur ne soit détruit. Cette dernière chance veut dire qu'une instruction "try ... finally" dans le générateur est maintenant assurée d'être utilisée, la clause finally a maintenant toujours une chance d'être lancée.

