Expressions de générateur contre compréhension de liste

412

Quand devez-vous utiliser des expressions de générateur et quand devez-vous utiliser des compréhensions de liste en Python?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]
Lecture seulement
la source
28
pourrait [exp for x in iter]être juste du sucre list((exp for x in iter))? ou y a-t-il une différence d'exécution?
b0fh
1
il pense que j'avais une question pertinente, donc lors de l'utilisation de yield, pouvons-nous utiliser uniquement l'expression générateur d'une fonction ou devons-nous utiliser yield pour une fonction pour renvoyer un objet générateur?
28
@ b0fh Réponse très tardive à votre commentaire: en Python2 il y a une petite différence, la variable de boucle sortira d'une compréhension de liste, tandis qu'une expression de générateur ne coulera pas. Comparez X = [x**2 for x in range(5)]; print xavec Y = list(y**2 for y in range(5)); print y, le second donnera une erreur. En Python3, une compréhension de liste est en effet le sucre syntaxique d'une expression de générateur alimentée list()comme vous vous y attendiez, de sorte que la variable de boucle ne s'échappera plus .
Bas Swinckels
13
Je suggère de lire le PEP 0289 . Résumée par "Ce PEP introduit des expressions de générateur comme une généralisation performante et efficace en mémoire des compréhensions de listes et des générateurs" . Il contient également des exemples utiles sur le moment de les utiliser.
icc97
5
@ icc97 J'ai également huit ans de retard pour la fête et le lien PEP était parfait. Merci d'avoir rendu cela facile à trouver!
eenblam

Réponses:

283

La réponse de John est bonne (les listes sont mieux comprises lorsque vous souhaitez répéter plusieurs fois quelque chose). Cependant, il convient également de noter que vous devez utiliser une liste si vous souhaitez utiliser l'une des méthodes de liste. Par exemple, le code suivant ne fonctionnera pas:

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

Fondamentalement, utilisez une expression de générateur si tout ce que vous faites est d'itérer une fois. Si vous souhaitez stocker et utiliser les résultats générés, alors vous êtes probablement mieux avec une liste de compréhension.

Étant donné que la performance est la raison la plus courante de choisir l'un plutôt que l'autre, mon conseil est de ne pas s'en inquiéter et d'en choisir un; si vous trouvez que votre programme s'exécute trop lentement, alors et seulement à ce moment-là, devriez-vous revenir en arrière et vous soucier de régler votre code.

Eli Courtwright
la source
70
Parfois, vous devez utiliser des générateurs - par exemple, si vous écrivez des coroutines avec une planification coopérative à l'aide de yield. Mais si vous faites cela, vous ne posez probablement pas cette question;)
éphémient
12
Je sais que c'est vieux, mais je pense qu'il vaut la peine de noter que les générateurs (et tout itérable) peuvent être ajoutés aux listes avec extend: a = [1, 2, 3] b = [4, 5, 6] a.extend(b)- un sera désormais [1, 2, 3, 4, 5, 6]. (Pouvez-vous ajouter des retours à la ligne dans les commentaires ??)
jarvisteve
12
@jarvisteve votre exemple dément les mots que vous dites. Il y a aussi un point fin ici. Les listes peuvent être étendues avec des générateurs, mais il n'était alors pas utile d'en faire un générateur. Les générateurs ne peuvent pas être étendus avec des listes, et les générateurs ne sont pas tout à fait itérables. a = (x for x in range(0,10)), b = [1,2,3]par exemple. a.extend(b)lève une exception. b.extend(a)évaluera tout un, auquel cas il est inutile d'en faire un générateur en premier lieu.
Slater Victoroff
4
@SlaterTyranus vous avez 100% raison, et je vous ai voté pour la précision. Néanmoins, je pense que son commentaire est une non-réponse utile à la question du PO car il aidera ceux qui se trouvent ici parce qu'ils ont tapé quelque chose comme 'combiner générateur avec compréhension de liste' dans un moteur de recherche.
rbp
1
La raison d'utiliser un générateur pour parcourir une fois (par exemple, ma préoccupation concernant le manque de mémoire l'emporte sur ma préoccupation de "récupérer" les valeurs une par une ) ne s'appliquerait-elle pas toujours lors de l'itération plusieurs fois? Je dirais que cela pourrait rendre une liste plus utile, mais si cela suffit pour l'emporter sur les problèmes de mémoire, c'est autre chose.
Rob Grant
181

Itérer sur l' expression du générateur ou la compréhension de la liste fera la même chose. Cependant, la compréhension de la liste créera d'abord la liste entière en mémoire tandis que l' expression du générateur créera les éléments à la volée, vous pouvez donc l'utiliser pour des séquences très grandes (et aussi infinies!).

dF.
la source
39
+1 pour l'infini. Vous ne pouvez pas faire cela avec une liste, peu importe le peu que vous vous souciez des performances.
Paul Draper
Pouvez-vous créer des générateurs infinis en utilisant la méthode de compréhension?
AnnanFay
5
@Annan Seulement si vous avez déjà accès à un autre générateur infini. Par exemple, itertools.count(n)est une séquence infinie d'entiers, à partir de n, (2 ** item for item in itertools.count(n))serait donc une séquence infinie des pouvoirs de 2départ à 2 ** n.
Kevin
2
Un générateur supprime les éléments de la mémoire après leur itération. Donc, c'est rapide si vous avez des données volumineuses, vous voulez simplement les afficher, par exemple. Ce n'est pas un porc de mémoire. avec les générateurs, les articles sont traités «au besoin». si vous voulez vous accrocher à la liste ou l'itérer à nouveau (alors stockez les articles), utilisez la compréhension de la liste.
j2emanue
102

Utilisez des compréhensions de liste lorsque le résultat doit être répété plusieurs fois ou lorsque la vitesse est primordiale. Utilisez des expressions de générateur où la plage est grande ou infinie.

Voir Expressions de générateur et compréhensions de liste pour plus d'informations.

John Millikin
la source
2
Ce sera probablement un peu hors sujet, mais malheureusement "non googlable" ... Que signifierait "primordial" dans ce contexte? Je ne suis pas natif anglophone ... :)
Guillermo Ares
6
@GuillermoAres c'est le résultat direct de "googler" pour le sens de primordial: plus important que toute autre chose; suprême.
Sнаđошƒаӽ
1
Les expressions listssont-elles donc plus rapides que les generatorexpressions? En lisant la réponse de dF, il est apparu que c'était l'inverse.
Hassan Baig
1
Il est probablement préférable de dire que les compréhensions de liste sont plus rapides lorsque la plage est petite, mais à mesure que l'échelle augmente, il devient plus utile de calculer les valeurs à la volée - juste à temps pour leur utilisation. C'est ce que fait une expression de générateur.
Kyle
59

L'important est que la compréhension de la liste crée une nouvelle liste. Le générateur crée un objet itérable qui "filtre" le matériel source à la volée lorsque vous consommez les bits.

Imaginez que vous ayez un fichier journal de 2 To appelé "énormefichier.txt" et que vous souhaitiez le contenu et la longueur de toutes les lignes commençant par le mot "ENTRY".

Vous essayez donc de commencer par écrire une liste de compréhension:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

Cela accélère le fichier entier, traite chaque ligne et stocke les lignes correspondantes dans votre tableau. Ce tableau peut donc contenir jusqu'à 2 To de contenu. C'est beaucoup de RAM, et ce n'est probablement pas pratique pour vos besoins.

Nous pouvons donc utiliser un générateur pour appliquer un "filtre" à notre contenu. Aucune donnée n'est réellement lue jusqu'à ce que nous commencions à répéter le résultat.

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

Pas même une seule ligne n'a encore été lue dans notre fichier. En fait, disons que nous voulons filtrer notre résultat encore plus loin:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

Rien n'a encore été lu, mais nous avons spécifié maintenant deux générateurs qui agiront sur nos données comme nous le souhaitons.

Permet d'écrire nos lignes filtrées dans un autre fichier:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

Maintenant, nous lisons le fichier d'entrée. Comme notre forboucle continue de demander des lignes supplémentaires, le long_entriesgénérateur demande des lignes au entry_linesgénérateur, renvoyant uniquement celles dont la longueur est supérieure à 80 caractères. Et à son tour, le entry_linesgénérateur demande des lignes (filtrées comme indiqué) à l' logfileitérateur, qui à son tour lit le fichier.

Ainsi, au lieu de "pousser" les données vers votre fonction de sortie sous la forme d'une liste entièrement remplie, vous donnez à la fonction de sortie un moyen de "tirer" les données uniquement lorsque cela est nécessaire. C'est dans notre cas beaucoup plus efficace, mais pas aussi flexible. Les générateurs sont à sens unique, un passage; les données du fichier journal que nous avons lu sont immédiatement supprimées, nous ne pouvons donc pas revenir à une ligne précédente. D'un autre côté, nous n'avons pas à nous soucier de conserver les données une fois que nous en avons terminé.

tylerl
la source
46

L'avantage d'une expression de générateur est qu'elle utilise moins de mémoire car elle ne construit pas la liste entière à la fois. Les expressions de générateur sont mieux utilisées lorsque la liste est un intermédiaire, comme la somme des résultats ou la création d'un dict à partir des résultats.

Par exemple:

sum(x*2 for x in xrange(256))

dict( (k, some_func(k)) for k in some_list_of_keys )

L'avantage est que la liste n'est pas complètement générée, et donc peu de mémoire est utilisée (et devrait également être plus rapide)

Vous devez cependant utiliser des compréhensions de liste lorsque le produit final souhaité est une liste. Vous n'allez pas enregistrer de mémoire à l'aide d'expressions de générateur, car vous voulez la liste générée. Vous avez également l'avantage de pouvoir utiliser n'importe quelle fonction de liste comme triée ou inversée.

Par exemple:

reversed( [x*2 for x in xrange(256)] )
Mandrin
la source
9
Il y a un indice pour vous dans le langage que les expressions de générateur sont censées être utilisées de cette façon. Perdez les crochets! sum(x*2 for x in xrange(256))
u0b34a0f6ae
8
sortedet reversedfonctionne très bien sur toutes les expressions de générateur itérables incluses.
marr75
1
Si vous pouvez utiliser 2.7 et au-dessus, cet exemple dict () serait mieux comme compréhension de dict (le PEP pour cela est plus ancien que les expressions de générateur PEP, mais a mis plus de temps à atterrir)
Jürgen A. Erhard
14

Lors de la création d'un générateur à partir d'un objet mutable (comme une liste), sachez que le générateur sera évalué sur l'état de la liste au moment de l'utilisation du générateur, et non au moment de la création du générateur:

>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing

S'il y a une chance que votre liste soit modifiée (ou un objet modifiable à l'intérieur de cette liste) mais vous avez besoin de l'état à la création du générateur, vous devez utiliser une compréhension de liste à la place.

freaker
la source
1
Et cela devrait être la réponse acceptée. Si vos données sont plus grandes que la mémoire disponible, vous devez toujours utiliser des générateurs bien que le bouclage sur la liste en mémoire puisse être plus rapide (mais vous n'avez pas assez de mémoire pour le faire).
Marek Marczak
4

Parfois, vous pouvez vous en tirer avec la fonction tee d' itertools , elle renvoie plusieurs itérateurs pour le même générateur qui peuvent être utilisés indépendamment.

Jacob Rigby
la source
4

J'utilise le module Hadoop Mincemeat . Je pense que c'est un excellent exemple pour prendre note de:

import mincemeat

def mapfn(k,v):
    for w in v:
        yield 'sum',w
        #yield 'count',1


def reducefn(k,v): 
    r1=sum(v)
    r2=len(v)
    print r2
    m=r1/r2
    std=0
    for i in range(r2):
       std+=pow(abs(v[i]-m),2)  
    res=pow((std/r2),0.5)
    return r1,r2,res

Ici, le générateur extrait des nombres d'un fichier texte (aussi gros que 15 Go) et applique des calculs simples à ces nombres à l'aide de la réduction de carte de Hadoop. Si je n'avais pas utilisé la fonction de rendement, mais plutôt une compréhension de liste, il aurait fallu beaucoup plus de temps pour calculer les sommes et la moyenne (sans parler de la complexité de l'espace).

Hadoop est un excellent exemple pour utiliser tous les avantages des générateurs.

Murphy
la source