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.
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.
Réponses:
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)
yield
et join
aprè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 yield
jusqu'à 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é.
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.
list(fibon(5))
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.
Je trouve cette explication qui dissipe mon doute. Parce qu'il est possible qu'une personne qui ne sait pas Generators
ne 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 function
où nous nous sommes arrêtés. C'est là que le concept de generators
sont introduits et la yield
déclaration reprend là où elle function
s'é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 return
etyield
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' yield
instruction 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).
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 domains
et le nom de la colonne est domain
.
Si vous utilisez, SELECT domain FROM domains
cela 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 yield
les résultats. Si vous utilisiez le mot-clé à la return
place 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()
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.
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 for
boucles et autres constructions d'itérations traditionnelles.
L'explication simple: considérez une for
déclaration
for item in iterable:
do_stuff()
La plupart du temps, tous les éléments iterable
ne 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
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.
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.
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.
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_corners
et 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' for
expression.
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.
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 yield
d'explication, générateurs pour le traitement parallèle, échapper à la limite de récursivité, etc.)
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.
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)