Question

Je commence à apprendre le 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 permettent vraiment de résoudre.

Était-ce utile?

La solution

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 considérer les générateurs comme renvoyant plusieurs éléments, comme s'ils renvoyaient 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 de la mémoire pour tous les résultats à le même temps. Ou dans les cas où le générateur utilise un autre générateur, ou utilise une autre ressource, ce qui 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. Au lieu d’écrire un rappel distinct et de le transmettre à la fonction de travail, l’appelant effectue tous les rapports en une petite boucle 'for' autour du générateur.

Par exemple, supposons que vous avez é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 consomme mieux la mémoire et est beaucoup plus convivial pour l'utilisateur. Cette dernière option peut être effectuée en passant la fonction d’impression des résultats à 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, voir 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.) Of Bien sûr, si vous voulez vraiment collecter 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)

Autres conseils

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 l’un après l’autre, 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 ceci:

# 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, mais une valeur à la fois. Ce ne serait pas le cas lorsque vous utiliseriez la version de liste, une liste étant créée en premier.

Voir le " Motivation " section du 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 travaux & "simultanément &"; (entrelacé, en fait) sans utiliser de thread.

Je trouve cette explication qui dissipe mon doute. Parce qu'il est possible qu'une personne ne sachant pas Generators ne sache pas non plus que 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 function reprend là où le return n'est pas suivi.

  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 <=> et <=> instructions en Python.

La déclaration de rendement est ce qui fait de la 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 <=> chaque fois qu’ils souhaitent 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).

Exemple concret

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 le nom de votre table est domains et le nom de la colonne est domain.

Si vous utilisez SELECT domain FROM domains, il retournera 100 millions de lignes, ce qui consommera beaucoup de mémoire. Votre serveur risque donc de 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 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 constater, notre fonction conserve yield les résultats. Si vous utilisiez le mot clé return au lieu de <=>, 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é <=>, alors c'est un générateur.

Vous pouvez maintenant 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()

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

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 en fonction de 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ù, 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 disposez maintenant de deux objets générateurs de numéros Fibonacci que vous pouvez appeler de n’importe où dans votre code. 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

Ce qui est bien avec les générateurs, c’est qu’ils encapsulent un état sans avoir à passer par les cerceaux de la création d’objets. Une façon de penser à eux est comme & "; Fonctions &"; qui se souviennent de leur état interne.

L’exemple Fibonacci a été tiré de Générateurs Python - Quoi sont-ils? et avec un peu d'imagination, vous pouvez trouver beaucoup d'autres situations dans lesquelles les générateurs constituent une excellente alternative aux for boucles et autres constructions d'itération traditionnelles.

L’explication simple: Considérons une for déclaration

for item in iterable:
   do_stuff()

Souvent, tous les éléments de iterable ne doivent pas nécessairement être présents dès le début, mais peuvent être générés à la volée en fonction de leurs besoins. Cela peut être beaucoup plus efficace dans les deux

  • espace (vous n'avez jamais besoin de stocker tous les éléments simultanément) et
  • time (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 belle boucle 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.

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

Disons que nous lisons un fichier et ne voulons que les lignes commençant par & "; ## &";.

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

Nous pouvons ensuite utiliser la fonction de 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 génératrice nous permet de filtrer ou de réduire une séquence, en produisant une autre séquence, une valeur à la fois.

Vous pouvez utiliser un générateur comme exemple pratique si vous avez une forme quelconque et que vous souhaitez effectuer une itération sur ses coins, ses bords ou autre. 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

Je peux maintenant 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 for puisque nous pouvons alors utiliser le nom de l'instance de la classe directement dans l'expression <=>.

Éviter les fonctions de rappel lors de l'itération sur le maintien de l'état de maintien de l'entrée.

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

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

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

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

David Beazley à propos des générateurs à la conférence PyCon 2014

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 simple générateur 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 des jeux de données volumineux (potentiellement infinis!) sans les générer à l’avance, ce qui n’est qu’une des utilisations les plus évidentes.

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)
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top