À quoi pouvez-vous utiliser les fonctions du générateur Python?

213

Je commence à apprendre Python et je suis tombé sur des fonctions de générateur, celles qui contiennent une déclaration de rendement. Je veux savoir quels types de problèmes ces fonctions sont vraiment bonnes à résoudre.

quamrana
la source
6
peut-être qu'une meilleure question serait de savoir quand nous ne devrions pas les utiliser
cregox
1
Exemple du monde réel ici
Giri

Réponses:

239

Les générateurs vous donnent une évaluation paresseuse. Vous les utilisez en les itérant, soit explicitement avec 'for', soit implicitement en les passant à n'importe quelle fonction ou construction qui itère. Vous pouvez considérer les générateurs comme renvoyant plusieurs éléments, comme s'ils renvoyaient une liste, mais au lieu de les renvoyer tous en même temps, ils les retournent un par un, et la fonction de générateur est suspendue jusqu'à ce que l'élément suivant soit demandé.

Les générateurs sont bons pour calculer de grands ensembles de résultats (en particulier les calculs impliquant des boucles elles-mêmes) lorsque vous ne savez pas si vous allez avoir besoin de tous les résultats, ou lorsque vous ne voulez pas allouer de la mémoire pour tous les résultats en même temps . Ou pour les situations où le générateur utilise un autre générateur, ou consomme une autre ressource, et c'est plus pratique si cela s'est produit le plus tard possible.

Une autre utilisation des générateurs (qui est vraiment la même) est de remplacer les rappels par l'itération. Dans certaines situations, vous souhaitez qu'une fonction effectue beaucoup de travail et fasse parfois rapport à l'appelant. Traditionnellement, vous utilisiez une fonction de rappel pour cela. Vous passez ce rappel à la fonction de travail et il appellera périodiquement ce rappel. L'approche du générateur est que la fonction de travail (maintenant un générateur) ne sait rien du rappel, et cède simplement chaque fois qu'elle veut signaler quelque chose. L'appelant, au lieu d'écrire un rappel séparé et de le transmettre à la fonction de travail, fait tout le travail de rapport dans une petite boucle "for" autour du générateur.

Par exemple, supposons que vous ayez écrit un programme de «recherche de système de fichiers». Vous pouvez effectuer la recherche dans son intégralité, collecter les résultats, puis les afficher un par un. Tous les résultats devraient être collectés avant que vous ne montriez le premier, et tous les résultats seraient en mémoire en même temps. Ou vous pouvez afficher les résultats pendant que vous les trouvez, ce qui serait plus efficace en mémoire et beaucoup plus convivial pour l'utilisateur. Ce dernier pourrait être fait en passant la fonction d'impression de résultat à la fonction de recherche de système de fichiers, ou cela pourrait être fait en faisant simplement de la fonction de recherche un générateur et en itérant sur le résultat.

Si vous souhaitez voir un exemple des deux dernières approches, consultez os.path.walk () (l'ancienne fonction de marche du système de fichiers avec rappel) et os.walk () (le nouveau générateur de marche du système de fichiers.) Bien sûr, si vous vouliez vraiment collecter tous les résultats dans une liste, l'approche du générateur est triviale à convertir à l'approche de la grande liste:

big_list = list(the_generator)
Thomas Wouters
la source
Un générateur tel que celui qui produit des listes de systèmes de fichiers effectue-t-il des actions en parallèle au code qui exécute ce générateur en boucle? Idéalement, l'ordinateur devrait exécuter le corps de la boucle (traiter le dernier résultat) tout en faisant ce que le générateur doit faire pour obtenir la valeur suivante.
Steven Lu
@StevenLu: À moins qu'il ne soit difficile de lancer manuellement les threads avant yieldet joinaprès pour obtenir le résultat suivant, il ne s'exécute pas en parallèle (et aucun générateur de bibliothèque standard ne le fait; le lancement secret de threads est mal vu). Le générateur s'arrête à chaque fois yieldjusqu'à ce que la valeur suivante soit demandée. Si le générateur encapsule les E / S, le système d'exploitation peut mettre en cache de manière proactive les données du fichier en supposant qu'il sera demandé sous peu, mais c'est le système d'exploitation, Python n'est pas impliqué.
ShadowRanger
90

L'une des raisons d'utiliser le générateur est de rendre la solution plus claire pour certains types de solutions.

L'autre consiste à traiter les résultats un par un, en évitant de créer d'énormes listes de résultats que vous traitez de toute façon.

Si vous avez une fonction fibonacci jusqu'à n comme celle-ci:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Vous pouvez plus facilement écrire la fonction comme ceci:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

La fonction est plus claire. Et si vous utilisez la fonction comme ceci:

for x in fibon(1000000):
    print x,

dans cet exemple, si vous utilisez la version du générateur, la liste complète de 1000000 éléments ne sera pas créée du tout, une seule valeur à la fois. Ce ne serait pas le cas lors de l'utilisation de la version liste, où une liste serait créée en premier.

nosklo
la source
18
et si vous avez besoin d'une liste, vous pouvez toujours le fairelist(fibon(5))
endolith
41

Voir la section "Motivation" dans PEP 255 .

Une utilisation non évidente des générateurs crée des fonctions interruptibles, qui vous permettent de faire des choses comme mettre à jour l'interface utilisateur ou exécuter plusieurs tâches "simultanément" (entrelacées, en fait) sans utiliser de threads.

Nickolay
la source
1
La section Motivation est intéressante en ce qu'elle contient un exemple spécifique: "Lorsqu'une fonction de producteur a un travail suffisamment difficile pour nécessiter le maintien de l'état entre les valeurs produites, la plupart des langages de programmation n'offrent aucune solution agréable et efficace au-delà de l'ajout d'une fonction de rappel à l'argument du producteur. list ... Par exemple, tokenize.py dans la bibliothèque standard adopte cette approche "
Ben Creasy
38

Je trouve cette explication qui dissipe mon doute. Parce qu'il est possible qu'une personne qui ne sait pas Generatorsne connaisse pasyield

Revenir

L'instruction return est l'endroit où toutes les variables locales sont détruites et la valeur résultante est rendue (retournée) à l'appelant. Si la même fonction est appelée quelque temps plus tard, la fonction obtiendra un nouvel ensemble de variables.

rendement

Mais que se passe-t-il si les variables locales ne sont pas supprimées lorsque nous quittons une fonction? Cela implique que nous pouvons resume the functionoù nous nous sommes arrêtés. C'est là que le concept de generatorssont introduits et la yielddéclaration reprend là où elle functions'était arrêtée.

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Voilà donc la différence entre returnetyield instructions en Python.

L'énoncé de rendement est ce qui fait d'une fonction une fonction de générateur.

Les générateurs sont donc un outil simple et puissant pour créer des itérateurs. Ils sont écrits comme des fonctions normales, mais ils utilisent l' yieldinstruction chaque fois qu'ils veulent renvoyer des données. Chaque fois que next () est appelé, le générateur reprend là où il s'était arrêté (il se souvient de toutes les valeurs de données et de la dernière instruction exécutée).

Mirage
la source
33

Exemple du monde réel

Disons que votre table MySQL contient 100 millions de domaines et que vous souhaitez mettre à jour le classement Alexa pour chaque domaine.

La première chose dont vous avez besoin est de sélectionner vos noms de domaine dans la base de données.

Disons que le nom de votre table est domainset le nom de la colonne est domain.

Si vous utilisez, SELECT domain FROM domainscela retournera 100 millions de lignes, ce qui consommera beaucoup de mémoire. Votre serveur pourrait donc se bloquer.

Vous avez donc décidé d'exécuter le programme par lots. Disons que notre taille de lot est de 1000.

Dans notre premier lot, nous interrogerons les 1000 premières lignes, vérifierons le classement Alexa pour chaque domaine et mettrons à jour la ligne de base de données.

Dans notre deuxième lot, nous travaillerons sur les 1000 lignes suivantes. Dans notre troisième lot, ce sera de 2001 à 3000 et ainsi de suite.

Maintenant, nous avons besoin d'une fonction de générateur qui génère nos lots.

Voici notre fonction de générateur:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Comme vous pouvez le voir, notre fonction conserve yieldles résultats. Si vous utilisiez le mot-clé à la returnplace de yield, alors la fonction entière serait terminée une fois qu'elle serait revenue.

return - returns only once
yield - returns multiple times

Si une fonction utilise le mot-clé yield c'est un générateur.

Vous pouvez maintenant répéter comme ceci:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()
Giri
la source
ce serait plus pratique, si le rendement pouvait être expliqué en termes de programmation récursive / dyanmique!
igaurav
27

Mise en mémoire tampon. Lorsqu'il est efficace de récupérer des données en gros morceaux, mais de les traiter en petits morceaux, un générateur peut aider:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

Ce qui précède vous permet de séparer facilement la mise en mémoire tampon du traitement. La fonction consommateur peut maintenant simplement obtenir les valeurs une par une sans se soucier de la mise en mémoire tampon.

Rafał Dowgird
la source
3
Si getBigChuckOfData n'est pas paresseux, je ne comprends pas quel est le rendement des avantages ici. Qu'est-ce qu'un cas d'utilisation pour cette fonction?
Sean Geoffrey Pietz
1
Mais le fait est que, IIUC, bufferedFetch est paresseux l'appel à getBigChunkOfData. Si getBigChunkOfData était déjà paresseux, alors bufferedFetch serait inutile. Chaque appel à bufferedFetch () retournera un élément tampon, même si un BigChunk a déjà été lu. Et vous n'avez pas besoin de compter explicitement le nombre de l'élément suivant à retourner, car la mécanique du rendement le fait implicitement.
hmijail pleure les démissionnaires
21

J'ai trouvé que les générateurs sont très utiles pour nettoyer votre code et en vous donnant un moyen très unique d'encapsuler et de modulariser le code. Dans une situation où vous avez besoin de quelque chose pour cracher constamment des valeurs en fonction de son propre traitement interne et lorsque ce quelque chose doit être appelé de n'importe où dans votre code (et pas seulement dans une boucle ou un bloc par exemple), les générateurs sont la fonctionnalité pour utilisation.

Un exemple abstrait serait un générateur de nombres de Fibonacci qui ne vit pas dans une boucle et quand il est appelé de n'importe où, il retournera toujours le numéro suivant dans la séquence:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Vous avez maintenant deux objets générateurs de nombres Fibonacci que vous pouvez appeler de n'importe où dans votre code et ils renverront toujours des nombres Fibonacci toujours plus grands dans l'ordre comme suit:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

La belle chose à propos des générateurs est qu'ils encapsulent l'état sans avoir à passer par les cerceaux de la création d'objets. Une façon de les considérer est comme des "fonctions" qui se souviennent de leur état interne.

J'ai obtenu l'exemple de Fibonacci de Python Generators - Quels sont-ils? et avec un peu d'imagination, vous pouvez trouver beaucoup d'autres situations où les générateurs constituent une excellente alternative aux forboucles et autres constructions d'itérations traditionnelles.

Andz
la source
19

L'explication simple: considérez une fordéclaration

for item in iterable:
   do_stuff()

La plupart du temps, tous les éléments iterablene doivent pas nécessairement être présents dès le début, mais peuvent être générés à la volée selon les besoins. Cela peut être beaucoup plus efficace à la fois

  • l'espace (vous n'avez jamais besoin de stocker tous les articles simultanément) et
  • temps (l'itération peut se terminer avant que tous les éléments soient nécessaires).

D'autres fois, vous ne connaissez même pas tous les articles à l'avance. Par exemple:

for command in user_input():
   do_stuff_with(command)

Vous n'avez aucun moyen de connaître toutes les commandes de l'utilisateur à l'avance, mais vous pouvez utiliser une belle boucle comme celle-ci si vous avez un générateur vous remettant des commandes:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

Avec les générateurs, vous pouvez également avoir une itération sur des séquences infinies, ce qui n'est bien sûr pas possible lors de l'itération sur des conteneurs.

dF.
la source
... et une séquence infinie pourrait être générée en parcourant à plusieurs reprises une petite liste, revenant au début une fois la fin atteinte. Je l'utilise pour sélectionner des couleurs dans les graphiques ou produire des lanceurs ou des filateurs occupés dans le texte.
Andrej Panjkov
@mataap: Il y en a un itertoolpour ça - voyez cycles.
martineau
12

Mes utilisations préférées sont les opérations de "filtrage" et de "réduction".

Disons que nous lisons un fichier et que nous voulons uniquement les lignes qui commencent par "##".

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Nous pouvons ensuite utiliser la fonction générateur dans une boucle appropriée

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

L'exemple de réduction est similaire. Disons que nous avons un fichier où nous devons localiser des blocs de <Location>...</Location>lignes. [Pas des balises HTML, mais des lignes qui ressemblent à des balises.]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Encore une fois, nous pouvons utiliser ce générateur dans une boucle for appropriée.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

L'idée est qu'une fonction de générateur nous permet de filtrer ou de réduire une séquence, produisant une autre séquence une valeur à la fois.

S.Lott
la source
8
fileobj.readlines()lirait l'intégralité du fichier dans une liste en mémoire, ce qui irait à l'encontre de l'utilisation des générateurs. Étant donné que les objets fichier sont déjà itérables, vous pouvez utiliser à la for b in your_generator(fileobject):place. De cette façon, votre fichier sera lu une ligne à la fois, pour éviter de lire tout le fichier.
nosklo le
ReduceLocation est assez bizarre, donnant une liste, pourquoi ne pas simplement donner chaque ligne? Le filtrage et la réduction sont également des commandes intégrées avec les comportements attendus (voir l'aide d'ipython, etc.), votre utilisation de "réduire" est identique à celle du filtre.
James Antill,
Bon point sur les readlines (). Je réalise généralement que les fichiers sont des itérateurs de ligne de première classe lors des tests unitaires.
S.Lott
En fait, la "réduction" combine plusieurs lignes individuelles en un objet composite. D'accord, c'est une liste, mais c'est toujours une réduction tirée de la source.
S.Lott
9

Un exemple pratique où vous pourriez utiliser un générateur est si vous avez une sorte de forme et que vous souhaitez parcourir ses coins, ses bords ou autre chose. Pour mon propre projet (code source ici ), j'avais un rectangle:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Maintenant, je peux créer un rectangle et une boucle sur ses coins:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

Au lieu de cela, __iter__vous pourriez avoir une méthode iter_cornerset l'appeler avec for corner in myrect.iter_corners(). Il est juste plus élégant à utiliser __iter__car nous pouvons alors utiliser le nom d'instance de classe directement dans l' forexpression.

Pithikos
la source
J'ai adoré l'idée de passer des champs de classe similaires en tant que générateur
eusoubrasileiro
7

Évitant essentiellement les fonctions de rappel lors de l'itération sur l'état de maintien d'entrée.

Voir ici et ici pour un aperçu de ce qui peut être fait en utilisant des générateurs.

MvdD
la source
4

Quelques bonnes réponses ici, cependant, je recommanderais également une lecture complète du didacticiel de programmation fonctionnelle Python qui aide à expliquer certains des cas d'utilisation les plus puissants des générateurs.

songololo
la source
3

Puisque la méthode d'envoi d'un générateur n'a pas été mentionnée, voici un exemple:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

Il montre la possibilité d'envoyer une valeur à un générateur en marche. Un cours plus avancé sur les générateurs dans la vidéo ci-dessous (y compris yieldd'explication, générateurs pour le traitement parallèle, échapper à la limite de récursivité, etc.)

David Beazley sur les générateurs à PyCon 2014

John Damen
la source
2

J'utilise des générateurs lorsque notre serveur Web agit en tant que proxy:

  1. Le client demande une URL proxy au serveur
  2. Le serveur commence à charger l'URL cible
  3. Le serveur cède pour renvoyer les résultats au client dès qu'il les obtient
Brian
la source
1

Des tas de trucs. Chaque fois que vous souhaitez générer une séquence d'éléments, mais ne voulez pas avoir à les «matérialiser» tous dans une liste à la fois. Par exemple, vous pourriez avoir un générateur simple qui renvoie des nombres premiers:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Vous pouvez ensuite l'utiliser pour générer les produits des nombres premiers suivants:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

Ce sont des exemples assez triviaux, mais vous pouvez voir comment cela peut être utile pour traiter de grands ensembles de données (potentiellement infinis!) Sans les générer à l'avance, ce qui n'est qu'une des utilisations les plus évidentes.

Nick Johnson
la source
sinon aucun (candidat% prime pour prime dans primes_found) devrait être si tout (candidat% prime pour prime dans primes_found)
rjmunro
Oui, je voulais écrire "sinon aucun (candidat% prime == 0 pour prime dans primes_found). Le tien est cependant un peu plus net. :)
Nick Johnson
Je suppose que vous avez oublié de supprimer le «pas» de sinon tous (candidat% prime pour prime dans primes_found)
Thava
0

Convient également pour l'impression des nombres premiers jusqu'à n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Sébastien Wieckowski
la source