Et avec plusieurs for ?
La dernière fois nous nous étions arrêtés là :
offset = 5
plage = 10
g = (offset + n for n in range(plage))
offset = 10
plage = 3
print(list(g))
affichera :
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
et pas [10, 11, 12]
car l’expression range(plage)
est évaluée au moment
où on crée l’expression génératrice.
Mais ça se corse si on utilise plusieurs for
:
maxu = 3
maxv = 3
l = ((u, v) for u in range(maxu) for v in range(maxv))
print(list(l))
le code précédent affiche 9 (3 x 3) éléments :
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
Mais par contre:
maxu = 3
maxv = 3
l = ((u, v) for u in range(maxu) for v in range(maxv))
maxu = 10
maxv = 10
print(list(l))
Ne donnera ni 9 (3x3) ni 100 (10x10) éléments, mais seulement 30.
En effet, si on a plusieurs for
dans une expression génératrice, seul le
premier, qui correspondrait au for
le plus
externe si on écrivait des boucles imbriquées est évalué au
moment de la création de l’expression
(cf PEP 289).
L’évaluation des expressions des autres for
est retardée.
Dans le cas qui précède, la valeur utilisée pour maxu
est celle référencée
au moment de la création de l’expression génératrice.
Alors que celle utilisée pour maxv
est celle référencée
au moment du parcours du générateur (c’est à dire au moment où on exécute list(g)
).
Et les fermetures dans tout ça ?
Une fermeture (closure) permet de capturer une référence à un objet, et de l’utiliser plus tard à un moment où l’objet n’est normalement plus visible (plus dans le scope).
Un exemple classique :
def ajoute(n):
def ajout(x):
return x + n
return ajout
La fonction ajoute
renvoie une autre fonction :
>>> aj = ajoute(10)
>>> aj(5)
15
>>> aj(3)
13
>>> n
NameError: name 'n' is not defined
La valeur référencée par le paramètre n
a été capturée, et elle est utilisée plus tard, lorsqu’on évalue aj(5)
et que le nom n
n’existe plus.
Avec les générateurs, le comportement est exactement ce qu’on attend. Voici une fonction qui renvoie un générateur, générant les multiples d’un nombre passé en paramètre à la fonction :
def multiples(n):
return (n * i for i in range(0, 10))
On utilise la fonction ainsi :
>>> m5 = multiples(5)
>>> m7 = multiples(7)
>>> list(m5)
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
>>> list(m7)
[0, 7, 14, 21, 28, 35, 42, 49, 56, 63]
Comme on n’a pas de moyen de modifier n
(enfin… je crois…) après
qu’on a récupéré le générateur renvoyé par multiples
, il n’y a pas de
difficulté particulière ici.