Question

Quand devez-vous utiliser des expressions génératrices 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)]
Était-ce utile?

La solution

La réponse de John est bonne (cette compréhension de la liste est meilleure lorsque vous souhaitez parcourir quelque chose plusieurs fois).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 génératrice 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, il est probablement préférable d'utiliser une compréhension de liste.

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

Autres conseils

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

Utilisez la compréhension de liste lorsque le résultat doit être itéré plusieurs fois ou lorsque la vitesse est primordiale.Utilisez des expressions génératrices dont la plage est grande ou infinie.

Le point 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 « filtrera » le matériel source à la volée au fur et à mesure que vous consommez les bits.

Imaginez que vous ayez un fichier journal de 2 To appelé « hugefile.txt » et que vous souhaitiez connaître le contenu et la longueur de toutes les lignes commençant par le mot « ENTRY ».

Alors vous essayez de commencer par rédiger une liste de compréhension :

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

Cela récupère l'intégralité du fichier, traite chaque ligne et stocke les lignes correspondantes dans votre tableau.Cette baie pourrait donc contenir jusqu'à 2 To de contenu.Cela représente beaucoup de RAM et 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 tant que nous n'avons pas commencé à parcourir 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 souhaitons filtrer encore plus notre résultat :

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

Toujours rien n'a été lu, mais nous avons désormais spécifié deux générateurs qui agiront sur nos données comme nous le souhaitons.

Écrivons 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 for la boucle continue de demander des lignes supplémentaires, la long_entries le générateur demande des lignes au entry_lines générateur, renvoyant uniquement ceux dont la longueur est supérieure à 80 caractères.Et à son tour, le entry_lines Le générateur demande des lignes (filtrées comme indiqué) à partir du logfile itérateur, qui lit à son tour le fichier.

Ainsi, au lieu de « transmettre » des données à votre fonction de sortie sous la forme d’une liste entièrement remplie, vous donnez à la fonction de sortie un moyen de « extraire » des 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, à passage unique ;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é avec elles.

L’avantage d’une expression génératrice est qu’elle utilise moins de mémoire puisqu’elle ne construit pas toute la liste en même temps.Les expressions génératrices 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 entièrement générée, et donc peu de mémoire est utilisée (et devrait également être plus rapide)

Vous devez cependant utiliser la compréhension de liste lorsque le produit final souhaité est une liste.Vous n'allez pas enregistrer de mémoire à l'aide d'expressions génératrices, puisque vous voulez la liste générée.Vous bénéficiez également de 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)] )

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 son utilisation, 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 mutable à l'intérieur de cette liste) mais que vous avez besoin de l'état lors de la création du générateur, vous devez plutôt utiliser une compréhension de liste.

j'utilise le Module Hadoop Mincemeat.Je pense que c'est un excellent exemple à prendre en note :

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 les nombres d'un fichier texte (pouvant atteindre 15 Go) et applique des calculs simples sur 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, le calcul des sommes et de la moyenne aurait pris beaucoup plus de temps (sans parler de la complexité spatiale).

Hadoop est un excellent exemple d'utilisation de tous les avantages des générateurs.

Parfois, on peut s'en sortir avec le tee fonction de itertools, il renvoie plusieurs itérateurs pour le même générateur qui peuvent être utilisés indépendamment.

que diriez-vous d'utiliser [(exp for x in iter)] pour obtenir le bien des deux.Performances issues de la compréhension du générateur ainsi que des méthodes de liste

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top