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.
Laisser un commentaire