Ce billet était initialement paru sur le blog Pydéfis.
Vous connaissez tous les listes en intention : [n**2 for n in range(10)]. Si on met des parenthèses à la place des crochets, plutôt qu’une liste, on obtient une expression génératrice. Mais attention aux pièges…

Pourquoi utiliser des expressions génératrices ?

Essentiellement, utiliser une expression génératrice permet de construire les éléments à la demande, plutôt qu’à l’avance. C’est utile si on veut économiser de la place mémoire (les éléments ne sont pas stockés tous à la fois en mémoire), et du temps (si on est intéressé uniquement par les premiers éléments… et qu’on ne le sait pas à l’avance…).

Le code suivant liste les 3 premiers carrés dont la somme des chiffres vaut 94 :

def sommechiffres(n):
    return sum(int(c) for c in str(n))

l = [n ** 2 for n in range(10000000) if sommechiffres(n ** 2) == 94]
print(l[:3])

En utilisant les listes en intention, tous les éléments sont construits au moment où la liste est évaluée et pas juste les 3 premiers. Ici, peu importe l’économie de place (il y a peu de carrés qui satisfont la condition… en réalité juste 3 qui soient dans la plage d’observation), mais on fait un peu trop de calcul, puisque le range(...) se «déroule» complètement.

La même chose avec une expression génératrice :

def sommechiffres(n):
    return sum(int(c) for c in str(n))

l = (n ** 2 for n in range(100000000) if sommechiffres(n ** 2) == 42)

for i in range(3):
    print(next(l))

Cette fois les calculs sont faits à la demande, lors d’un appel à next. On ne fait que les calculs nécessaires pour obtenir les 3 premiers nombres convenables.

Le piège

Comme les générateurs sont évalués au plus tard, si on utilise des références dans les expressions à calculer, ces références ne sont résolues qu’au moment où on parcourt l’expression génératrice, et non au moment où on la crée.

Par exemple :

offset = 5 
g = (offset + n for n in range(10))

print(list(g))

affichera sans surprise:

[5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

mais par contre:

offset = 5 
g = (offset + n for n in range(10))

offset = 10
print(list(g))

affichera :

[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

Si on remplace dans ce qui précède les expressions génératrices par des listes:

offset = 5 
g = [offset + n for n in range(10)]

offset = 10
print(g)

On a la même chose qu’on change ou non la valeur offset après la création de g:

[5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

Attention, c’est bien l’expression à générer qui est retardée, et pas celle du for:

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.

À suivre