Ce billet était initialement paru sur le blog Pydéfis.

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.