web-dev-qa-db-fra.com

Que pouvez-vous utiliser Python) fonctions du générateur?

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 efficaces pour résoudre. .

204
quamrana

Les générateurs vous donnent une évaluation paresseuse. Vous les utilisez en itérant dessus, soit explicitement avec 'pour', soit implicitement en le transmettant à une fonction ou à une construction qui itère. Vous pouvez penser que les générateurs renvoient plusieurs éléments, comme s'ils renvoient une liste, mais au lieu de tous les renvoyer en une fois, ils les renvoient un par un et la fonction du générateur est suspendue jusqu'à ce que l'élément suivant soit demandé.

Les générateurs sont utiles pour calculer de grands ensembles de résultats (en particulier des 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 souhaitez pas allouer la mémoire pour tous les résultats en même temps. . Ou pour les situations où le générateur utilise n autre générateur, ou consomme une autre ressource, et il est plus pratique si cela se produit le plus tard possible.

Une autre utilisation des générateurs (qui est vraiment la même chose) est de remplacer les rappels par itération. Dans certaines situations, vous souhaitez qu'une fonction effectue beaucoup de travail et fasse parfois rapport à l'appelant. Traditionnellement, vous utiliseriez une fonction de rappel pour cela. Vous transmettez ce rappel à la fonction de travail et l'appelera périodiquement. L’approche de 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, effectue la totalité des rapports en une boucle "pour" 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 devront être collectés avant que vous ne montriez le premier, et tous les résultats seront en mémoire en même temps. Vous pouvez également afficher les résultats pendant que vous les trouvez, ce qui permettrait d’optimiser la mémoire et d’être beaucoup plus convivial pour l’utilisateur. Ce dernier pourrait être effectué en passant la fonction d’impression du résultat à la fonction de recherche du système de fichiers, ou simplement en transformant la fonction de recherche en générateur et en effectuant une itération sur le résultat.

Si vous souhaitez voir un exemple de ces 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 rassembler tous les résultats dans une liste, l’approche du générateur est simple à convertir en approche de la grande liste:

big_list = list(the_generator)
227
Thomas Wouters

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

L’autre consiste à traiter les résultats un à un, en évitant de créer d’énormes listes de résultats que vous traiteriez de manière séparée.

Si vous avez une fonction fibonacci-up-to-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 totalité de la liste des éléments 1000000 ne sera pas créée du tout, mais une valeur à la fois. Ce ne serait pas le cas lors de l'utilisation de la version de liste, où une liste serait créée en premier.

87
nosklo

Voir la section "Motivation" dans PEP 255 .

Une utilisation non évidente des générateurs est la création de fonctions interruptibles, qui vous permettent d'effectuer des tâches telles que la mise à jour de l'interface utilisateur ou l'exécution de plusieurs tâches "simultanément" (en fait, entrelacées) sans utiliser de threads.

41
Nickolay

Je trouve cette explication qui efface mon doute. Parce qu'il est possible qu'une personne qui ne connaît pas Generators ne connaisse pas non plus yield

Retour

L'instruction return indique où toutes les variables locales sont détruites et la valeur résultante est renvoyée (renvoyée) à l'appelant. Si la même fonction est appelée ultérieurement, elle recevra un nouvel ensemble de variables.

rendement

Mais que se passe-t-il si les variables locales ne sont pas rejetées lorsque nous quittons une fonction? Cela implique que nous pouvons resume the function où nous nous sommes arrêtés. C’est là que le concept de generators est introduit et que l’instruction yield reprend là où le function s’est arrêté.

  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 les instructions return et yield en Python.

L'instruction de rendement est ce qui fait d'une fonction une fonction génératrice.

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’instruction yield 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).

37
Mirage

Exemple du monde réel

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

La première chose à faire est de sélectionner vos noms de domaine dans la base de données.

Disons que votre nom de table est domains et que le nom de colonne est domain.

Si tu utilises SELECT domain FROM domains il retournera 100 millions de lignes, ce qui consommera beaucoup de mémoire. Donc, votre serveur peut tomber en panne.

Vous avez donc décidé de lancer le programme par lots. Disons que la taille de notre 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 la base de données.

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

Nous avons maintenant besoin d’une fonction génératrice 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 constater, notre fonction garde yielding les résultats. Si vous utilisiez le mot clé return au lieu de yield, alors la fonction entière serait terminée une fois qu'elle aurait été renvoyée.

return - returns only once
yield - returns multiple times

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

Maintenant, vous pouvez itérer 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()
31
Giri

Mise en mémoire tampon. Lorsqu'il est efficace d'extraire des données par gros morceaux, mais de les traiter par 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 désormais obtenir les valeurs une par une sans se soucier de la mise en mémoire tampon.

26
Rafał Dowgird

J'ai constaté que les générateurs sont très utiles pour nettoyer votre code et vous donner un moyen très unique d'encapsuler et de modulariser le code. Dans une situation où vous avez besoin de quelque chose qui crache constamment des valeurs basées sur son propre traitement interne et qui 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é à utiliser.

Un exemple abstrait serait un générateur de nombre 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()

Maintenant, vous avez deux objets générateurs de numéros 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 bonne 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 penser à eux est comme des "fonctions" qui se souviennent de leur état interne.

J'ai eu l'exemple de Fibonacci de Générateurs Python - Que sont-ils? et avec un peu d'imagination, vous pouvez créer un Beaucoup d'autres situations où les générateurs constituent une excellente alternative aux boucles for et aux autres constructions d'itération traditionnelles.

21
Andz

L'explication simple: considérons une instruction for

for item in iterable:
   do_stuff()

La plupart du temps, tous les éléments de iterable n'ont pas besoin d'être présents dès le début, mais peuvent être générés à la volée comme ils sont nécessaires. Cela peut être beaucoup plus efficace dans les deux

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

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

for command in user_input():
   do_stuff_with(command)

Vous n'avez aucun moyen de connaître au préalable toutes les commandes de l'utilisateur, mais vous pouvez utiliser une boucle de Nice comme celle-ci si vous avez un générateur qui vous remet des commandes:

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

Avec les générateurs, vous pouvez également effectuer une itération sur des séquences infinies, ce qui n’est bien entendu pas possible lors d’une itération sur des conteneurs.

18
dF.

Mes utilisations préférées sont les opérations "filtrer" et "réduire".

Disons que nous lisons un fichier et ne voulons que 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 dans lequel nous devons localiser des blocs de <Location>...</Location> lignes. [Pas les balises HTML, mais les 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.

12
S.Lott

Un exemple pratique où vous pourriez utiliser un générateur est le cas où vous avez une forme quelconque et que vous souhaitez parcourir les coins, les 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 faire une boucle sur ses coins:

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

Au lieu de __iter__, Vous pouvez utiliser une méthode iter_corners Et l'appeler avec for corner in myrect.iter_corners(). Il est juste plus élégant d'utiliser __iter__, Car nous pouvons utiliser le nom de l'instance de la classe directement dans l'expression for.

8
Pithikos

En gros, éviter les fonctions de rappel lors d'une itération sur l'état de maintien des entrées.

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

7
MvdD

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

4
songololo

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 reçoit.
2
Brian

Comme 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 cours d'exécution. Un cours plus avancé sur les générateurs dans la vidéo ci-dessous (comprenant yield de explination, générateurs pour traitement parallèle, échappant à la limite de récursivité, etc.)

David Beazley sur les générateurs à la conférence PyCon 2014

2
John Damen

Des tas de choses. Chaque fois que vous souhaitez générer une séquence d'éléments, mais que vous ne voulez pas avoir à les "matérialiser" tous dans une liste à la fois. Par exemple, vous pourriez avoir un générateur simple qui retourne 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 en quoi 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 que l’une des utilisations les plus évidentes.

1
Nick Johnson

Aussi bien pour imprimer les 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)
0