[python] itérateur vs générateur

[python] itérateur vs générateur

En python, nous retrouvons les concepts d’itérateurs et de générateurs. Vous utilisez sûrement déjà les itérateurs couramment sans même savoir comment les nommer. C’est important de savoir que ces deux concepts existent, car ils ont des répercussions différentes au niveau de la mémoire. Pour les petits programmes qui traitent de petits jeux de données, pas de problème. Mais pour les gros jeux de données, c’est autre chose. Que sont-ils au juste?

Les itérateurs

Quand vous parcourez les éléments d’une liste un par un, on appelle cela l’itération:

 
>>> lst = [1,2,3]
>>> for i in lst:
...  print(i*i)

1
4
9

Et quand on utilise une liste en intention (list comprehension), on créé une liste, donc un objet itérable.

 
>>> lst = [x*x for x in [1,2,3]]  # [1, 4, 9]
>>> print(lst)

[1, 4, 9]
 
>>> for i in lst:
...  print(i)

1
4
9

Les générateurs

En remplaçant les []  par des  (), on crée des expressions génératrices (on ne crée plus de liste).
Avec les générateurs, les données ne sont pas sauvegardées en mémoire dans la variable lst, mais vont être générées à la volée (à la demande ou au fur et à mesure).
Attention: La génération des données à la volée ne permet pas de les relire une seconde fois et si vous essayez tout de même, aucune erreur ne sera produite pour vous prévenir.

 
>>> lst = (x*x for x in [1,2,3])  # [1, 4, 9]
>>> print(lst)

<generator object <genexpr> at 0x1933640>

Ici « genexpr » signifie generator expression (et non gene expression pour les biologistes).

 
>>> for i in lst:
...  print(i)

1
4
9
 
>>> for i in lst:
...  print(i)


# Rien ne s'est affiché

Vous remarquerez qu’on désire imprimer la liste lst deux fois, mais que les résultats ne s’affiche qu’une seule fois.

Astuce: Quand on commence à apprécier l’utilisation des générateurs, il est particulièrement intéressant de les imbriquer (performance, lisibilité du code). Mais attention, la lecture de la deuxième liste lst2 effacera la première!

 
>>> lst1 = (x*x for x in [1,2,3])   # [1, 4, 9]
>>> lst2 = (x+x for x in lst1)  # [2, 8, 18]
>>> for i in lst2:
...  print(i)

2
8
18
 
>>> for i in lst1:
...  print(i)


# Rien ne s'est affiché

Avantages/inconvénients des générateurs

Si vous avez besoin d’accéder une seule fois à vos données, vous allez gagner de l’espace mémoire (les données sont générées à la demande) et votre programme sera plus rapide.

Maintenant, si vous devez accéder plusieurs fois à vos données, vous allez toujours gagner en espace mémoire, mais votre programme sera plus lent (les données devront être regénérées à chaque demande et il faut recréer les générateurs avant chaque demande). Il est généralement déconseillé d’utiliser les générateurs dans ce derniers cas.

Pour finir, voici un petit exemple pour vous donner une idée de la performance des générateurs quand ils sont correctement utilisés.

 
import os
import gc
import psutil

num = 10000000
rep = 500

def mem_usage_in_MB(proc):
  return  proc.memory_info()[0] / float(2 ** 20)

proc = psutil.Process(os.getpid())
mem0 = mem_usage_in_MB(proc)
toto = (x*x for x in range(num))
tata = (x+x for x in toto)
tutu = (x-1 for x in tata)
print("mem generator: " + str(mem_usage_in_MB(proc) - mem0) + "MB")
mem0 = mem_usage_in_MB(proc)
toto = [x*x for x in range(num)]
toto = [x+x for x in toto]
toto = [x-1 for x in toto]
print("mem iterator: " + str(mem_usage_in_MB(proc) - mem0) + "MB")


import timeit
def test(t, num):
  toto = (x*x for x in range(num)) if t == "gen" else [x*x for x in range(num)]
  sum(toto)

def test2(t, num):
  toto = (x*x for x in range(num)) if t == "gen" else [x*x for x in range(num)]
  toto = (x+x for x in toto) if t == "gen" else [x+x for x in toto]
  toto = (x-1 for x in toto) if t == "gen" else [x-1 for x in toto]
  sum(toto)

print("test time generator:" + str(timeit.timeit("test(\"gen\"," + str(num) + ")", setup="from __main__ import test", number=rep)))
print("test time iterator:" + str(timeit.timeit("test(\"iter\"," + str(num) + ")", setup="from __main__ import test", number=rep)))
print("test2 time generator:" + str(timeit.timeit("test(\"gen\"," + str(num) + ")", setup="from __main__ import test", number=rep)))
print("test2 time iterator:" + str(timeit.timeit("test(\"iter\"," + str(num) + ")", setup="from __main__ import test", number=rep)))
# avec python3

mem generator: 0.00390625MB
mem iterator: 387.8984375MB
test time generator:730.7246094942093
test time iterator:765.0462868176401
test2 time generator:727.7452643960714
test2 time iterator:768.4699434302747

# avec python2

mem generator: 310.72265625MB
mem iterator: 545.578125MB
test time generator:801.186733007
test time iterator:757.989295006
test2 time generator:810.537645102
test2 time iterator:939.240092993

 

Note: Ceci n’est qu’une introduction sur les générateurs, vus par le biais des listes en intention (très utilisées par les programmeurs python). Pour en savoir plus, je vous invite à regarder cette présentation et à vous documenter sur le mot clé yield.
Note 2: Avec Python2 il faut remplacez « range » par « xrange » pour obtenir une meilleure performance, mais le code ne sera plus compatible Python3.

By | 2017-04-29T17:11:30+00:00 18 septembre 2015|Categories: Performance, Python|0 Commentaires

About the Author:

Informaticien de formation, j’ai vite compris que la bioinformatique regorge d’égnimes à résoudre. Comme dans le « Sommet des dieux »(Jiro Taniguchi), il y a toujours un nouveau sommet à gravir ou un itinéraire plus direct à tenter.

Laisser un commentaire