Quelle est l'utilisation du mot clé yield
en Python? Qu'est ce que ça fait?
Par exemple, j'essaie de comprendre ce code1:
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
Et voici l'appelant:
result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
Que se passe-t-il lorsque la méthode _get_child_candidates
est appelée? Une liste est-elle renvoyée? Un seul élément? Est-il appelé à nouveau? Quand les appels suivants vont-ils s'arrêter?
1. Le code provient de Jochen Schulz (jrschulz), qui a créé une excellente bibliothèque Python pour les espaces métriques. Voici le lien vers la source complète: Module mspace .
Pour comprendre ce que fait yield
, vous devez comprendre ce que sont les {générateurs}. Et avant que les générateurs arrivent iterables.
Lorsque vous créez une liste, vous pouvez lire ses éléments un à un. La lecture de ses éléments un à un s'appelle itération:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
est un itérable. Lorsque vous utilisez une compréhension de liste, vous créez une liste, et donc une variable:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Tout ce que vous pouvez utiliser "for... in...
" est un itératif; lists
, strings
, fichiers ...
Ces itérables sont pratiques car vous pouvez les lire autant que vous le souhaitez, mais vous stockez toutes les valeurs en mémoire et ce n’est pas toujours ce que vous voulez lorsque vous avez beaucoup de valeurs.
Les générateurs sont des itérateurs, une sorte d'iterable que vous ne pouvez parcourir qu'une fois. Les générateurs ne stockent pas toutes les valeurs en mémoire, ils génèrent les valeurs à la volée:
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
C'est la même chose sauf que vous avez utilisé ()
au lieu de []
. MAIS, vous ne pouvez pas exécuter for i in mygenerator
une seconde fois, car les générateurs ne peuvent être utilisés qu’une fois: ils calculent 0, puis l’oublient et calculent 1, et terminent le calcul de 4, un par un.
yield
est un mot clé utilisé comme return
, sauf que la fonction renvoie un générateur.
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
Ici, c’est un exemple inutile, mais c’est pratique lorsque vous savez que votre fonction renverra un très grand nombre de valeurs que vous ne devrez lire qu’une fois.
Pour maîtriser yield
, vous devez comprendre que lorsque vous appelez la fonction, le code que vous avez écrit dans le corps de la fonction ne s'exécute pas. La fonction ne renvoie que l'objet générateur, c'est un peu compliqué :-)
Ensuite, votre code continuera d’où il s’est arrêté chaque fois que for
utilise le générateur.
Maintenant la partie difficile:
La première fois que la variable for
appelle l'objet générateur créé à partir de votre fonction, le code de votre fonction sera exécuté depuis le début jusqu'à ce qu'il atteigne la variable yield
, puis il renverra la première valeur de la boucle. Ensuite, chaque appel exécutera à nouveau la boucle que vous avez écrite dans la fonction et renverra la valeur suivante, jusqu'à ce qu'il n'y ait plus de valeur à renvoyer.
Le générateur est considéré comme vide une fois la fonction exécutée, mais ne touche plus yield
. Cela peut être dû à la fin de la boucle ou à la non satisfaction d'un "if/else"
.
Générateur:
# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):
# Here is the code that will be called each time you use the generator object:
# If there is still a child of the node object on its left
# AND if distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# If there is still a child of the node object on its right
# AND if distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# If the function arrives here, the generator will be considered empty
# there is no more than two values: the left and the right children
Votre interlocuteur:
# Create an empty list and a list with the current object reference
result, candidates = list(), [self]
# Loop on candidates (they contain only one element at the beginning)
while candidates:
# Get the last candidate and remove it from the list
node = candidates.pop()
# Get the distance between obj and the candidate
distance = node._get_dist(obj)
# If distance is ok, then you can fill the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# Add the children of the candidate in the candidates list
# so the loop will keep running until it will have looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
Ce code contient plusieurs parties intelligentes:
La boucle itère sur une liste, mais la liste se développe pendant que la boucle est itérée :-) C'est une façon concise de parcourir toutes ces données imbriquées même si c'est un peu dangereux, car vous pouvez vous retrouver avec une boucle infinie. Dans ce cas, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
épuise toutes les valeurs du générateur, mais while
continue à créer de nouveaux objets générateur qui produiront des valeurs différentes des précédentes car elles ne sont pas appliquées sur le même nœud.
La méthode extend()
est une méthode d'objet de liste qui attend un itératif et ajoute ses valeurs à la liste.
Habituellement, nous lui passons une liste:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Mais dans votre code, il y a un générateur, ce qui est bien parce que:
Et cela fonctionne parce que Python ne se soucie pas de savoir si l'argument d'une méthode est une liste ou non. Python attend des iterables, donc il fonctionnera avec des chaînes, des listes, des n-uplets et des générateurs! Cela s’appelle dactylographie et c’est l’une des raisons pour lesquelles Python est si cool. Mais ceci est une autre histoire, pour une autre question ...
Vous pouvez vous arrêter ici ou lire un peu pour voir une utilisation avancée d'un générateur:
>>> class Bank(): # Let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
Remarque: Pour Python 3, utilisezprint(corner_street_atm.__next__())
ou print(next(corner_street_atm))
Cela peut être utile pour diverses choses comme le contrôle de l'accès à une ressource.
Le module itertools contient des fonctions spéciales pour manipuler les iterables. Avez-vous déjà souhaité dupliquer un générateur? Chaîne de deux générateurs? Grouper les valeurs dans une liste imbriquée avec un one-liner? Map / Zip
sans créer une autre liste?
Alors juste import itertools
.
Un exemple? Voyons les ordres d'arrivée possibles pour une course de quatre chevaux:
>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]
L'itération est un processus impliquant des itérables (implémentation de la méthode __iter__()
) et des itérateurs (implémentant la méthode __next__()
) . Les itérateurs sont des objets qui vous permettent d'itérer sur des itérables.
Il y en a plus dans cet article à propos de comment les boucles for
fonctionnent .
yield
Lorsque vous voyez une fonction avec des instructions yield
, appliquez cette astuce simple pour comprendre ce qui va se passer:
result = []
au début de la fonction.yield expr
par result.append(expr)
.return result
au bas de la fonction.yield
déclarations! Lire et comprendre le code.Cette astuce peut vous donner une idée de la logique derrière la fonction, mais ce qui se passe réellement avec yield
est très différent de ce qui se passe dans l’approche par liste. Dans de nombreux cas, l’approche de rendement sera beaucoup plus efficace en termes de mémoire et plus rapide. Dans d'autres cas, cette astuce vous bloquera dans une boucle infinie, même si la fonction d'origine fonctionne parfaitement. Continuez à lire pour en savoir plus...
Tout d'abord, le protocole iterator - lorsque vous écrivez
for x in mylist:
...loop body...
Python effectue les deux étapes suivantes:
Obtient un itérateur pour mylist
:
Call iter(mylist)
-> Ceci retourne un objet avec une méthode next()
(ou __next__()
en Python 3).
[C'est l'étape que la plupart des gens oublient de vous dire]
Utilise l'itérateur pour faire une boucle sur les éléments:
Continuez à appeler la méthode next()
sur l'itérateur renvoyé à l'étape 1. La valeur renvoyée par next()
est affectée à x
et le corps de la boucle est exécuté. Si une exception StopIteration
est générée à l'intérieur de next()
, cela signifie qu'il n'y a plus de valeurs dans l'itérateur et que la boucle est sortie.
En réalité, Python effectue les deux étapes ci-dessus chaque fois qu'il le souhaite boucle terminée le contenu d'un objet. Il peut donc s'agir d'une boucle for, mais il peut également s'agir d'un code comme otherlist.extend(mylist)
(où otherlist
est une liste Python ).
Ici mylist
est un itérable _ parce qu'il implémente le protocole itérateur. Dans une classe définie par l'utilisateur, vous pouvez implémenter la méthode __iter__()
pour rendre les instances de votre classe itérables. Cette méthode doit retourner un itérateur. Un itérateur est un objet avec une méthode next()
. Il est possible d'implémenter __iter__()
et next()
sur la même classe et d'avoir __iter__()
return self
. Cela fonctionnera pour des cas simples, mais pas si vous souhaitez que deux itérateurs bouclent le même objet en même temps.
C'est donc le protocole itérateur, de nombreux objets implémentent ce protocole:
__iter__()
.Notez qu'une boucle for
ne sait pas quel type d'objet elle traite - elle ne fait que suivre le protocole itérateur, et est heureuse d'obtenir élément après élément car elle appelle next()
. Les listes intégrées renvoient leurs éléments un par un, les dictionnaires renvoient les clés un par un, les fichiers renvoient les lignes un par un, etc. Et les générateurs reviennent ... yield
entre:
def f123():
yield 1
yield 2
yield 3
for item in f123():
print item
Au lieu des instructions yield
, si vous aviez trois instructions return
dans f123()
, seul le premier serait exécuté et la fonction se fermerait. Mais f123()
n’est pas une fonction ordinaire. Lorsque f123()
est appelé, il ne le fait pas ne renvoie aucune des valeurs des déclarations de rendement! Il retourne un objet générateur. En outre, la fonction ne quitte pas vraiment - elle passe dans un état suspendu. Lorsque la boucle for
tente de parcourir l'objet générateur, la fonction reprend son état suspendu à la ligne suivante après la yield
à partir de laquelle elle est retournée, exécute la ligne suivante de code, dans ce cas une instruction yield
, et la renvoie sous la forme suivante: l'élément suivant. Cela se produit jusqu'à la sortie de la fonction, moment auquel le générateur lève StopIteration
et la boucle se ferme.
Ainsi, l'objet générateur ressemble à un adaptateur: à une extrémité, il présente le protocole d'itérateur, en exposant les méthodes __iter__()
et next()
pour que la boucle for
reste heureuse. Cependant, à l’autre extrémité, il exécute la fonction juste assez pour en extraire la valeur suivante et la remet en mode suspendu.
Généralement, vous pouvez écrire du code qui n'utilise pas de générateurs mais implémente la même logique. Une option consiste à utiliser la "astuce" de la liste temporaire que j'ai mentionnée auparavant. Cela ne fonctionnera pas dans tous les cas, par exemple si vous avez des boucles infinies, ou que vous aurez une utilisation inefficace de la mémoire si vous avez une très longue liste. L'autre approche consiste à implémenter une nouvelle classe itérative SomethingIter
qui conserve l'état dans les membres de l'instance et effectue l'étape logique suivante dans sa méthode next()
(ou __next__()
en Python 3). Selon la logique, le code dans la méthode next()
peut paraître très complexe et être sujet à des bogues. Ici, les générateurs fournissent une solution propre et facile.
Pense-y de cette façon:
Un itérateur est juste un terme de qualité pour un objet qui a une méthode next (). Donc, une fonction cédée finit par ressembler à ceci:
Version originale:
def some_function():
for i in xrange(4):
yield i
for i in some_function():
print i
C’est essentiellement ce que l’interpréteur Python fait avec le code ci-dessus:
class it:
def __init__(self):
# Start at -1 so that we get 0 when we add 1 below.
self.count = -1
# The __iter__ method will be called once by the 'for' loop.
# The rest of the magic happens on the object returned by this method.
# In this case it is the object itself.
def __iter__(self):
return self
# The next method will be called repeatedly by the 'for' loop
# until it raises StopIteration.
def next(self):
self.count += 1
if self.count < 4:
return self.count
else:
# A StopIteration exception is raised
# to signal that the iterator is done.
# This is caught implicitly by the 'for' loop.
raise StopIteration
def some_func():
return it()
for i in some_func():
print i
Pour plus d'informations sur ce qui se passe dans les coulisses, la boucle for
peut être réécrite comme suit:
iterator = some_func()
try:
while 1:
print iterator.next()
except StopIteration:
pass
Cela a-t-il plus de sens ou vous perturbe-t-il davantage? :)
Je devrais noter que ceci est / une simplification excessive à des fins d’illustration. :)
Le mot clé yield
est réduit à deux faits simples:
yield
n'importe où à l'intérieur d'une fonction, cette fonction ne sera plus renvoyée via l'instruction return
. À la place, il immédiatement renvoie un objet lazy "liste en attente" appelé un générateurlist
ou set
ou range
ou dict-view, avec un protocole intégré permettant de visiter chaque élément dans un certain ordre.En un mot: un générateur est une liste en attente par incrément de manière incrémentielle, et les instructions yield
vous permettent d’utiliser la notation de fonction pour programmer les valeurs de liste que le générateur doit cracher de manière incrémentielle.
generator = myYieldingFunction(...)
x = list(generator)
generator
v
[x[0], ..., ???]
generator
v
[x[0], x[1], ..., ???]
generator
v
[x[0], x[1], x[2], ..., ???]
StopIteration exception
[x[0], x[1], x[2]] done
list==[x[0], x[1], x[2]]
Définissons une fonction makeRange
qui ressemble à range
de Python. Appeler makeRange(n)
RETOURNE UN GÉNÉRATEUR:
def makeRange(n):
# return 0,1,2,...,n-1
i = 0
while i < n:
yield i
i += 1
>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>
Pour forcer le générateur à renvoyer immédiatement ses valeurs en attente, vous pouvez le transférer dans list()
(comme vous pouvez le faire avec une variable quelconque):
>>> list(makeRange(5))
[0, 1, 2, 3, 4]
L’exemple ci-dessus peut être considéré comme une simple création d’une liste à ajouter et à renvoyer:
# list-version # # generator-version
def makeRange(n): # def makeRange(n):
"""return [0,1,2,...,n-1]""" #~ """return 0,1,2,...,n-1"""
TO_RETURN = [] #>
i = 0 # i = 0
while i < n: # while i < n:
TO_RETURN += [i] #~ yield i
i += 1 # i += 1 ## indented
return TO_RETURN #>
>>> makeRange(5)
[0, 1, 2, 3, 4]
Il y a cependant une différence majeure. voir la dernière section.
Un itérable est la dernière partie d'une liste de compréhension, et tous les générateurs sont itérables, ils sont donc souvent utilisés comme ceci:
# _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]
Pour avoir une meilleure idée des générateurs, vous pouvez jouer avec le module itertools
(assurez-vous d'utiliser chain.from_iterable
plutôt que chain
lorsque cela est justifié). Par exemple, vous pouvez même utiliser des générateurs pour implémenter des listes paresseuses infiniment longues telles que itertools.count()
. Vous pouvez implémenter votre propre def enumerate(iterable): Zip(count(), iterable)
ou le faire avec le mot clé yield
dans une boucle while.
Remarque: les générateurs peuvent en réalité être utilisés pour beaucoup d'autres choses, telles que la mise en œuvre de coroutines ou une programmation non déterministe ou d'autres choses élégantes. Cependant, le point de vue "listes paresseuses" que je présente ici est l'utilisation la plus courante que vous trouverez.
Voici comment fonctionne le "protocole d'itération Python". C'est-à-dire, que se passe-t-il quand vous faites list(makeRange(5))
. C'est ce que je décris plus tôt comme une "liste incrémentale paresseuse".
>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
La fonction intégrée next()
appelle simplement la fonction objects .next()
, qui fait partie du "protocole d'itération" et se trouve sur tous les itérateurs. Vous pouvez utiliser manuellement la fonction next()
(ainsi que d'autres parties du protocole d'itération) pour implémenter des éléments fantaisistes, généralement au détriment de la lisibilité. Essayez donc d'éviter de le faire ...
Normalement, la plupart des gens ne se soucient pas des distinctions suivantes et veulent probablement arrêter de lire ici.
En Python, un iterable est un objet qui "comprend le concept d'une boucle for", comme une liste [1,2,3]
, et un itérateur est une instance spécifique de la requête for- boucle comme [1,2,3].__iter__()
. Un générateur est exactement le même que n'importe quel itérateur, à l'exception de la façon dont il a été écrit (avec la syntaxe de la fonction).
Lorsque vous demandez un itérateur dans une liste, un nouvel itérateur est créé. Cependant, lorsque vous demandez un itérateur à un itérateur (ce que vous feriez rarement), il vous en donne simplement une copie.
Ainsi, dans le cas improbable où vous ne feriez pas quelque chose comme ça ...
> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]
... alors rappelez-vous qu'un générateur est un itérateur; c'est-à-dire qu'il s'agit d'un usage unique. Si vous souhaitez le réutiliser, vous devez appeler à nouveau myRange(...)
. Si vous devez utiliser le résultat deux fois, convertissez-le en liste et stockez-le dans une variable x = list(myRange(5))
. Ceux qui ont absolument besoin de cloner un générateur (par exemple, qui font une métaprogrammation terrifiante) peuvent utiliser itertools.tee
si cela est absolument nécessaire, puisque la proposition en copie de Python PEP en copie a été différée.
yield
est comme return
- il retourne tout ce que vous lui dites (en tant que générateur). La différence est que la prochaine fois que vous appelez le générateur, l'exécution commence à partir du dernier appel à l'instruction yield
. Contrairement à return, le cadre de la pile n’est pas nettoyé lorsqu’un rendement se produit, mais le contrôle est transféré à l’appelant. Son état est rétabli lors du prochain appel de la fonction.
Dans le cas de votre code, la fonction get_child_candidates
agit comme un itérateur. Ainsi, lorsque vous étendez votre liste, elle ajoute un élément à la fois à la nouvelle liste.
list.extend
appelle un itérateur jusqu'à ce qu'il soit épuisé. Dans le cas de l'exemple de code que vous avez posté, il serait beaucoup plus clair de simplement renvoyer un tuple et de l'ajouter à la liste.
Il y a une chose supplémentaire à mentionner: une fonction qui rend ne doit pas réellement se terminer. J'ai écrit un code comme ceci:
def fib():
last, cur = 0, 1
while True:
yield cur
last, cur = cur, last + cur
Ensuite, je peux l'utiliser dans un autre code comme celui-ci:
for f in fib():
if some_condition: break
coolfuncs(f);
Cela aide vraiment à simplifier certains problèmes et rend certaines choses plus faciles à gérer.
Pour ceux qui préfèrent un exemple de travail minimal, méditez sur cette session interactive Python :
>>> def f():
... yield 1
... yield 2
... yield 3
...
>>> g = f()
>>> for i in g:
... print i
...
1
2
3
>>> for i in g:
... print i
...
>>> # Note that this time nothing was printed
TL; DR
def square_list(n):
the_list = [] # Replace
for x in range(n):
y = x * x
the_list.append(y) # these
return the_list # lines
def square_yield(n):
for x in range(n):
y = x * x
yield y # with this one.
Chaque fois que vous vous créez une liste à partir de rien, yield
chaque pièce à la place.
Ce fut mon premier moment "aha" avec rendement.
yield
est un sucré moyen de dire
construire une série de choses
Même comportement:
>>> for square in square_list(4):
... print(square)
...
0
1
4
9
>>> for square in square_yield(4):
... print(square)
...
0
1
4
9
Comportement différent:
Le rendement est single-pass: vous ne pouvez parcourir qu'une seule fois. Quand une fonction a un rendement, on appelle cela une fonction generator . Et un itérateur est ce qu’il retourne. Ces termes sont révélateurs. Nous perdons la commodité d'un conteneur, mais bénéficions de la puissance d'une série calculée au besoin et arbitrairement longue.
Le rendement est lazy, cela retarde le calcul. Une fonction avec un rendement dedans ne s'exécute pas du tout lorsque vous l'appelez. Elle renvoie un objet iterator qui se souvient de l'endroit où il s'était arrêté. Chaque fois que vous appelez next()
sur l'itérateur (cela se produit dans une boucle for), l'exécution avance petit à petit au prochain rendement. return
lève StopIteration et termine la série (c'est la fin naturelle d'une boucle for).
Le rendement est polyvalent. Les données ne doivent pas être stockées toutes ensemble, elles peuvent être mises à disposition une par une. Cela peut être infini.
>>> def squares_all_of_them():
... x = 0
... while True:
... yield x * x
... x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
... print(next(squares))
...
0
1
4
9
Si vous avez besoin de multiple passes et que la série ne soit pas trop longue, appelez simplement list()
:
>>> list(square_yield(4))
[0, 1, 4, 9]
Choix génial du mot yield
parce que les deux significations s'appliquent:
rendement - produire ou fournir (comme dans l'agriculture)
... fournir les prochaines données de la série.
céder - céder ou abandonner (comme dans le pouvoir politique)
... abandonner l'exécution du processeur jusqu'à ce que l'itérateur avance.
À mon avis, il n’ya pas encore eu de type de réponse parmi les nombreuses excellentes qui décrivent comment utiliser des générateurs. Voici la réponse de la théorie du langage de programmation:
L'instruction yield
dans Python renvoie un générateur. Un générateur dans Python est une fonction qui retourne les continuations (et spécifiquement un type de coroutine, mais les continuations représentent le mécanisme plus général pour comprendre ce qui se passe).
Les suites dans la théorie des langages de programmation sont un type de calcul beaucoup plus fondamental, mais elles ne sont pas souvent utilisées, car elles sont extrêmement difficiles à raisonner et très difficiles à mettre en œuvre. Mais l’idée de ce qu’est une continuation est simple: c’est l’état d’un calcul qui n’est pas encore terminé. Dans cet état, les valeurs actuelles des variables, les opérations non encore exécutées, etc., sont enregistrées. Ensuite, à un moment ultérieur du programme, la suite peut être appelée, de sorte que les variables du programme sont réinitialisées à cet état et que les opérations sauvegardées sont exécutées.
Les continuations, sous cette forme plus générale, peuvent être mises en œuvre de deux manières. De la manière call/cc
, la pile du programme est littéralement sauvegardée puis, lorsque la continuation est invoquée, la pile est restaurée.
Dans la suite passant style (CPS), les continuations ne sont que des fonctions normales (uniquement dans les langages où les fonctions sont de première classe) que le programmeur gère explicitement et transfère aux sous-routines. Dans ce style, l'état du programme est représenté par des fermetures (et les variables qui y sont codées) plutôt que par des variables qui résident quelque part sur la pile. Les fonctions qui gèrent le flux de contrôle acceptent la continuation comme arguments (dans certaines variantes de CPS, les fonctions peuvent accepter plusieurs continuations) et manipulent le flux de contrôle en les appelant simplement en les appelant et en y retournant par la suite. Un exemple très simple de style de passage de continuation est le suivant:
def save_file(filename):
def write_file_continuation():
write_stuff_to_file(filename)
check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)
Dans cet exemple (très simpliste), le programmeur enregistre l'opération consistant à écrire le fichier dans une continuation (ce qui peut être potentiellement une opération très complexe avec beaucoup de détails à écrire), puis passe cette continuation (c'est-à-dire en tant que premier). fermeture de classe) à un autre opérateur qui effectue un traitement supplémentaire, puis l’appelle si nécessaire. (J'utilise beaucoup ce modèle de conception dans la programmation d'interface graphique, soit parce qu'il m'enregistre des lignes de code, soit, plus important encore, pour gérer le flux de contrôle après le déclenchement d'événements d'interface graphique.)
Le reste de cet article va, sans perte de généralité, conceptualiser les suites en tant que CPS, car il est extrêmement facile à comprendre et à lire.
Parlons maintenant des générateurs en Python. Les générateurs sont un sous-type spécifique de continuation. Alors que les continuations peuvent en général sauvegarder l'état d'un calcul (c'est-à-dire, la pile d'appels du programme ), les générateurs ne peuvent sauvegarder l’état d’itération que sur un itérateur . Bien que cette définition soit légèrement trompeuse pour certains cas d'utilisation de générateurs. Par exemple:
def f():
while True:
yield 4
Ceci est clairement un itératif raisonnable dont le comportement est bien défini - chaque fois que le générateur itère dessus, il retourne 4 (et le fait pour toujours). Mais ce n'est probablement pas le type prototypique d'itérable qui vient à l'esprit lorsqu'on pense aux itérateurs (c'est-à-dire, for x in collection: do_something(x)
). Cet exemple illustre la puissance des générateurs: s'il s'agit d'un itérateur, un générateur peut enregistrer l'état de son itération.
Pour réitérer: les continuations peuvent sauvegarder l'état de la pile d'un programme et les générateurs peuvent sauvegarder l'état de l'itération. Cela signifie que les continuations sont beaucoup plus puissantes que les générateurs, mais également que les générateurs sont beaucoup plus faciles. Celles-ci sont plus faciles à implémenter par le concepteur de langage et plus faciles à utiliser pour le programmeur (si vous avez du temps à graver, essayez de lire et de comprendre cette page sur les continuations et call/cc ).
Mais vous pouvez facilement implémenter (et conceptualiser) des générateurs comme un cas simple et spécifique de style de passage de continuation:
Chaque fois que yield
est appelé, il indique à la fonction de renvoyer une suite. Lorsque la fonction est appelée à nouveau, elle commence où qu'elle se soit arrêtée. Ainsi, en pseudo-pseudo-code (c'est-à-dire pas pseudo-code, mais pas code), la méthode next
du générateur est essentiellement la suivante:
class Generator():
def __init__(self,iterable,generatorfun):
self.next_continuation = lambda:generatorfun(iterable)
def next(self):
value, next_continuation = self.next_continuation()
self.next_continuation = next_continuation
return value
où le mot clé yield
est en réalité un sucre syntaxique pour la fonction de générateur réelle, ce qui revient à dire:
def generatorfun(iterable):
if len(iterable) == 0:
raise StopIteration
else:
return (iterable[0], lambda:generatorfun(iterable[1:]))
Rappelez-vous qu'il ne s'agit que d'un pseudocode et que la mise en œuvre réelle des générateurs dans Python est plus complexe. Mais comme exercice pour comprendre ce qui se passe, essayez d'utiliser le style de passage de continuation pour implémenter des objets générateurs sans utiliser le mot clé yield
.
Le rendement vous donne un générateur.
def get_odd_numbers(i):
return range(1, i, 2)
def yield_odd_numbers(i):
for x in range(1, i, 2):
yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5
Comme vous pouvez le constater, dans le premier cas, foo conserve la liste complète en mémoire en une fois. Ce n'est pas un gros problème pour une liste à 5 éléments, mais si vous voulez une liste de 5 millions? Non seulement c'est un gros mangeur de mémoire, mais sa construction coûte beaucoup de temps au moment où la fonction est appelée. Dans le second cas, bar vous donne simplement un générateur. Un générateur est un itératif - ce qui signifie que vous pouvez l'utiliser dans une boucle for, etc., mais chaque valeur n'est accessible qu'une seule fois. De plus, toutes les valeurs ne sont pas stockées en mémoire en même temps. l'objet générateur "se souvient" de l'endroit où il se trouvait dans la boucle la dernière fois que vous l'avez appelé - de cette façon, si vous utilisez un nombre itérable pour (par exemple) compter jusqu'à 50 milliards, vous n'avez pas à compter jusqu'à 50 milliards à la fois et stocker les 50 milliards de chiffres à compter. Encore une fois, c’est un bel exemple artificiel: vous utiliseriez probablement itertools si vous vouliez vraiment compter jusqu’à 50 milliards. :)
C'est le cas d'utilisation le plus simple des générateurs. Comme vous l'avez dit, il peut être utilisé pour écrire des permutations efficaces, en utilisant le rendement pour pousser des choses à travers la pile d'appels au lieu d'utiliser une sorte de variable de pile. Les générateurs peuvent également être utilisés pour la traversée d’arbres spécialisés, entre autres choses.
Il retourne un générateur. Je ne connais pas très bien Python, mais je pense que c'est le même genre de chose que les blocs d'itérateurs de C # si vous les connaissez bien.
L'idée clé est que le compilateur/interprète/quoi que ce soit fasse quelques ruses pour que l'appelant puisse continuer à appeler next () et qu'il continue à renvoyer des valeurs - comme si la méthode du générateur était en pause. Maintenant, évidemment, vous ne pouvez pas vraiment "mettre en pause" une méthode, aussi le compilateur construit-il une machine à états pour que vous puissiez vous rappeler où vous en êtes et à quoi ressemblent les variables locales, etc. C'est beaucoup plus facile que d'écrire un itérateur vous-même.
Voici un exemple en langage clair. Je ferai correspondre les concepts humains de haut niveau aux concepts Python de bas niveau.
Je veux opérer sur une séquence de nombres, mais je ne veux pas me déranger avec la création de cette séquence, je veux seulement me concentrer sur l'opération que je veux faire. Donc, je fais ce qui suit:
def
de la fonction du générateur, c’est-à-dire à la fonction contenant un yield
..next()
sur l'objet générateur.StopIteration
La fonction de générateur n'a pas besoin de déclencher l'exception. Il est déclenché automatiquement lorsque la fonction se termine ou émet une return
.C’est ce que fait un générateur (une fonction qui contient une yield
); il commence à s'exécuter, met en pause chaque fois qu'il fait une variable yield
, et lorsqu'on lui demande une valeur .next()
, il continue à partir du point où il se trouvait la dernière fois. Il s’intègre parfaitement de par sa conception au protocole itérateur de Python, qui explique comment demander des valeurs de manière séquentielle.
L'utilisateur le plus connu du protocole itérateur est la commande for
en Python. Donc, chaque fois que vous faites un:
for item in sequence:
peu importe si sequence
est une liste, une chaîne, un dictionnaire ou un générateur object comme décrit ci-dessus; le résultat est le même: vous lisez les éléments d'une séquence, l'un après l'autre.
Notez que def
infigurer une fonction contenant un mot clé yield
n’est pas le seul moyen de créer un générateur; c'est simplement le moyen le plus simple d'en créer un.
Pour des informations plus précises, consultez types d'itérateurs , les instructions de rendement et générateurs de la documentation Python.
Bien que beaucoup de réponses montrent pourquoi vous utiliseriez une yield
pour créer un générateur, il existe d'autres utilisations de yield
. Il est assez facile de créer une coroutine, ce qui permet de passer des informations entre deux blocs de code. Je ne répéterai aucun des beaux exemples déjà donnés sur l'utilisation de yield
pour créer un générateur.
Pour vous aider à comprendre ce que fait une yield
dans le code suivant, vous pouvez utiliser votre doigt pour suivre le cycle dans tout code comportant un yield
. Chaque fois que votre doigt touche la variable yield
, vous devez attendre la saisie d'une variable next
ou send
. Quand une next
est appelée, vous suivez le code jusqu'à ce que vous ayez appuyé sur la yield
… le code situé à droite de la yield
soit évalué et renvoyé à l'appelant… puis vous attendez. Lorsque next
est appelé à nouveau, vous effectuez une autre boucle dans le code. Cependant, vous remarquerez que, dans une coroutine, yield
peut également être utilisé avec un send
… qui enverra une valeur de l'appelant dans la fonction de cession. Si send
est donné, alors yield
reçoit la valeur envoyée et la recrache du côté gauche… puis la trace dans le code progresse jusqu'à ce que vous atteigniez de nouveau la yield
(renvoyant la valeur à la fin, comme si next
était appelé).
Par exemple:
>>> def coroutine():
... i = -1
... while True:
... i += 1
... val = (yield i)
... print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()
Il existe une autre utilisation yield
et sa signification (depuis Python 3.3):
yield from <expr>
De PEP 380 - Syntaxe de délégation dans un sous-générateur:
Une syntaxe est proposée à un générateur pour déléguer une partie de ses opérations à un autre générateur. Cela permet de factoriser une section de code contenant «rendement» et de la placer dans un autre générateur. En outre, le sous-générateur est autorisé à renvoyer une valeur, laquelle est mise à la disposition du générateur qui délègue.
La nouvelle syntaxe offre également des possibilités d'optimisation lorsqu'un générateur renvoie des valeurs produites par un autre.
De plus this introduira (depuis Python 3.5):
async def new_coroutine(data):
...
await blocking_action()
éviter que les routines ne soient confondues avec un générateur régulier (on utilise aujourd'hui yield
dans les deux cas).
Toutes les bonnes réponses, mais un peu difficile pour les débutants.
Je suppose que vous avez appris la déclaration return
.
Par analogie, return
et yield
sont des jumeaux. return
signifie 'retour et arrêt' alors que 'rendement' signifie 'retour, mais continue'
- Essayez d’obtenir une liste num_list avec
return
.
def num_list(n):
for i in range(n):
return i
Exécuter:
In [5]: num_list(3)
Out[5]: 0
Vous voyez, vous n'obtenez qu'un seul numéro au lieu d'une liste d'entre eux. return
ne vous permet jamais de l'emporter avec bonheur, il suffit de le mettre en œuvre une fois et d'arrêter.
- Il vient
yield
Remplacez return
par yield
:
In [10]: def num_list(n):
...: for i in range(n):
...: yield i
...:
In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>
In [12]: list(num_list(3))
Out[12]: [0, 1, 2]
Maintenant, vous gagnez pour obtenir tous les chiffres.
Si vous comparez à return
qui fonctionne une fois et s’arrête, yield
exécute les heures planifiées . Vous pouvez interpréter return
comme return one of them
et yield
comme return all of them
. Ceci s'appelle iterable
.
- Une étape supplémentaire nous pouvons réécrire l'instruction
yield
avecreturn
In [15]: def num_list(n):
...: result = []
...: for i in range(n):
...: result.append(i)
...: return result
In [16]: num_list(3)
Out[16]: [0, 1, 2]
C'est le noyau de yield
.
La différence entre une sortie de la liste return
et la sortie de l'objet yield
est la suivante:
Vous obtiendrez toujours [0, 1, 2] d'un objet de la liste, mais vous ne pourrez les récupérer qu'une seule fois à partir de 'l'objet yield
output'. Ainsi, il a un nouvel objet generator
nommé, comme indiqué dans Out[11]: <generator object num_list at 0x10327c990>
.
En conclusion, comme métaphore pour le dire:
return
et yield
sont des jumeauxlist
et generator
sont des jumeauxVoici quelques exemples Python montrant comment implémenter des générateurs comme si Python ne leur fournissait pas de sucre syntaxique:
En tant que générateur Python:
from itertools import islice
def fib_gen():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))
Utiliser des fermetures lexicales au lieu de générateurs
def ftake(fnext, last):
return [fnext() for _ in xrange(last)]
def fib_gen2():
#funky scope due to python2.x workaround
#for python 3.x use nonlocal
def _():
_.a, _.b = _.b, _.a + _.b
return _.a
_.a, _.b = 0, 1
return _
assert [1,1,2,3,5] == ftake(fib_gen2(), 5)
Utilisation de fermetures d'objet au lieu de générateurs (car ClosuresAndObjectsAreEquivalent )
class fib_gen3:
def __init__(self):
self.a, self.b = 1, 1
def __call__(self):
r = self.a
self.a, self.b = self.b, self.a + self.b
return r
assert [1,1,2,3,5] == ftake(fib_gen3(), 5)
J'allais poster "lire la page 19 de" Python: Référence essentielle "de Beazley pour une description rapide des générateurs", mais beaucoup d'autres ont déjà publié de bonnes descriptions.
En outre, notez que yield
peut être utilisé dans les routines comme le double de leur utilisation dans les fonctions du générateur. Bien que ce ne soit pas la même utilisation que votre extrait de code, (yield)
peut être utilisé comme expression dans une fonction. Lorsqu'un appelant envoie une valeur à la méthode à l'aide de la méthode send()
, la coroutine est exécutée jusqu'à la prochaine instruction (yield)
.
Les générateurs et les routines sont un moyen pratique de configurer des applications de type flux de données. Je pensais qu'il valait la peine de connaître l'autre utilisation de la déclaration yield
dans les fonctions.
Du point de vue de la programmation, les itérateurs sont implémentés sous la forme thunks .
Pour implémenter des itérateurs, des générateurs et des pools de threads pour une exécution simultanée, etc. sous la forme de thunks (également appelées fonctions anonymes), on utilise les messages envoyés à un objet de fermeture, qui possède un répartiteur, qui répond aux "messages".
_ { http://en.wikipedia.org/wiki/Message_passing } _
"next" est un message envoyé à une fermeture, créé par l'appel "iter".
Il y a beaucoup de façons d'implémenter ce calcul. J'ai utilisé mutation, mais il est facile de le faire sans mutation, en renvoyant la valeur actuelle et le prochain rendement.
Voici une démonstration qui utilise la structure de R6RS, mais la sémantique est absolument identique à celle de Python. C'est le même modèle de calcul, et seul un changement de syntaxe est nécessaire pour le réécrire en Python.
Welcome to Racket v6.5.0.3. -> (define gen (lambda (l) (define yield (lambda () (if (null? l) 'END (let ((v (car l))) (set! l (cdr l)) v)))) (lambda(m) (case m ('yield (yield)) ('init (lambda (data) (set! l data) 'OK)))))) -> (define stream (gen '(1 2 3))) -> (stream 'yield) 1 -> (stream 'yield) 2 -> (stream 'yield) 3 -> (stream 'yield) 'END -> ((stream 'init) '(a b)) 'OK -> (stream 'yield) 'a -> (stream 'yield) 'b -> (stream 'yield) 'END -> (stream 'yield) 'END ->
Voici un exemple simple:
def isPrimeNumber(n):
print "isPrimeNumber({}) call".format(n)
if n==1:
return False
for x in range(2,n):
if n % x == 0:
return False
return True
def primes (n=1):
while(True):
print "loop step ---------------- {}".format(n)
if isPrimeNumber(n): yield n
n += 1
for n in primes():
if n> 10:break
print "wiriting result {}".format(n)
Sortie:
loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call
Je ne suis pas un développeur Python, mais il me semble que yield
conserve la position de déroulement du programme et que la boucle suivante part de la position "rendement". Il semble qu’il attende à cette position et, juste avant, renvoie une valeur à l’extérieur et continue de fonctionner.
Cela semble être une capacité intéressante et agréable: D
Voici une image mentale de ce que yield
fait.
J'aime penser qu'un thread possède une pile (même s'il n'est pas implémenté de cette façon).
Lorsqu'une fonction normale est appelée, elle place ses variables locales sur la pile, effectue des calculs, efface la pile et retourne. Les valeurs de ses variables locales ne sont plus jamais vues.
Avec une fonction yield
, lorsque son code commence à s'exécuter (c'est-à-dire après l'appel de la fonction, en renvoyant un objet générateur, dont la méthode next()
est ensuite appelée), il place également ses variables locales dans la pile et les calcule pendant un certain temps. Mais ensuite, lorsqu'il atteint l'instruction yield
, avant d'effacer sa partie de la pile et de la renvoyer, il prend un instantané de ses variables locales et les stocke dans l'objet générateur. Il écrit également l'endroit où il se trouve actuellement dans son code (c'est-à-dire l'instruction yield
particulière).
Il s’agit donc d’une fonction figée à laquelle le générateur est suspendu.
Lorsque next()
est appelé par la suite, il récupère les objets de la fonction dans la pile et les réanime. La fonction continue de calculer à partir de là où elle s'était arrêtée, oubliant le fait qu'elle venait de passer une éternité dans un entrepôt frigorifique.
Comparez les exemples suivants:
def normalFunction():
return
if False:
pass
def yielderFunction():
return
if False:
yield 12
Lorsque nous appelons la deuxième fonction, elle se comporte très différemment de la première. La déclaration yield
peut être inaccessible, mais si elle est présente n'importe où, elle change la nature de ce à quoi nous sommes confrontés.
>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>
L'appel de yielderFunction()
ne lance pas son code, mais en fait un générateur. (Peut-être que c'est une bonne idée de nommer de telles choses avec le préfixe yielder
pour plus de lisibilité.)
>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
...
'__iter__', #Returns gen itself, to make it work uniformly with containers
... #when given to a for loop. (Containers return an iterator instead.)
'close',
'gi_code',
'gi_frame',
'gi_running',
'next', #The method that runs the function's body.
'send',
'throw']
Les champs gi_code
et gi_frame
sont ceux dans lesquels l'état figé est stocké. En les explorant avec dir(..)
, nous pouvons confirmer que notre modèle mental ci-dessus est crédible.
Comme chaque réponse le suggère, yield
est utilisé pour créer un générateur de séquence. Il est utilisé pour générer une séquence dynamiquement. Par exemple, lors de la lecture d'un fichier ligne par ligne sur un réseau, vous pouvez utiliser la fonction yield
comme suit:
def getNextLines():
while con.isOpen():
yield con.read()
Vous pouvez l'utiliser dans votre code comme suit:
for line in getNextLines():
doSomeThing(line)
Transfert du contrôle d'exécution exécuté
Le contrôle d'exécution sera transféré de getNextLines () à la boucle for
lorsque le rendement est exécuté. Ainsi, chaque fois que getNextLines () est appelée, l'exécution commence à partir du point où elle avait été suspendue la dernière fois.
Donc, en bref, une fonction avec le code suivant
def simpleYield():
yield "first time"
yield "second time"
yield "third time"
yield "Now some useful value {}".format(12)
for i in simpleYield():
print i
imprimera
"first time"
"second time"
"third time"
"Now some useful value 12"
Le rendement est un objet
Un return
dans une fonction retournera une valeur unique.
Si vous voulez une fonction retourne un très grand nombre de valeurs , utilisez yield
.
Plus important encore, yield
est un obstacle .
comme la barrière dans la langue CUDA, il ne transférera pas le contrôle jusqu'à ce qu'il obtienne terminé.
En d’autres termes, le code sera exécuté dans votre fonction depuis le début jusqu’à atteindre la variable yield
. Ensuite, il retournera la première valeur de la boucle.
Ensuite, chaque autre appel exécutera la boucle que vous avez écrite dans la fonction une autre fois, renvoyant la valeur suivante jusqu'à ce qu'il ne reste plus aucune valeur à renvoyer.
En résumé, l'instruction yield
transforme votre fonction en une fabrique qui produit un objet spécial appelé generator
qui enveloppe le corps de votre fonction d'origine. Lorsque la variable generator
est itérée, la fonction est exécutée jusqu'à atteindre la variable yield
suivante, puis l'exécution est suspendue et la valeur passée à yield
est évaluée. Il répète ce processus à chaque itération jusqu'à ce que le chemin d'exécution quitte la fonction. Par exemple,
def simple_generator():
yield 'one'
yield 'two'
yield 'three'
for i in simple_generator():
print i
simplement des sorties
one
two
three
La puissance provient de l’utilisation du générateur avec une boucle qui calcule une séquence, le générateur exécute la boucle en s’arrêtant à chaque fois pour «donner» le résultat suivant du calcul, ce qui permet de calculer une liste à la volée, l’avantage étant la mémoire. enregistré pour des calculs particulièrement volumineux
Supposons que vous vouliez créer votre propre fonction range
qui produit une plage de nombres itérable, vous pouvez le faire comme ça,
def myRangeNaive(i):
n = 0
range = []
while n < i:
range.append(n)
n = n + 1
return range
et l'utiliser comme ça;
for i in myRangeNaive(10):
print i
Mais c'est inefficace parce que
Heureusement, Guido et son équipe ont eu la générosité de développer des générateurs afin que nous puissions le faire.
def myRangeSmart(i):
n = 0
while n < i:
yield n
n = n + 1
return
for i in myRangeSmart(10):
print i
Désormais, à chaque itération, une fonction du générateur appelée next()
exécute la fonction jusqu'à ce qu'elle atteigne une instruction 'yield' dans laquelle elle s'arrête et 'renvoie' la valeur ou atteint la fin de la fonction. Dans ce cas, lors du premier appel, next()
exécute jusqu’à la déclaration de rendement et cède 'n', lors du prochain appel, il exécutera la déclaration d’incrémentation, retournera à la requête 'while', l’évaluera et, si elle est vraie, s’arrêtera et céder 'n' à nouveau, cela continuera jusqu'à ce que la condition while retourne false et que le générateur passe à la fin de la fonction.
Beaucoup de gens utilisent return
plutôt que yield
, mais dans certains cas, yield
peut être plus efficace et plus facile à utiliser.
Voici un exemple pour lequel yield
est certainement le meilleur pour:
retour (en fonction)
import random
def return_dates():
dates = [] # With 'return' you need to create a list then return it
for i in range(5):
date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
dates.append(date)
return dates
rendement (en fonction)
def yield_dates():
for i in range(5):
date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
yield date # 'yield' makes a generator automatically which works
# in a similar way. This is much more efficient.
Fonctions d'appel
dates_list = return_dates()
print(dates_list)
for i in dates_list:
print(i)
dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
print(i)
Les deux fonctions font la même chose, mais yield
utilise trois lignes au lieu de cinq et a une variable de moins à prendre en compte.
Ceci est le résultat du code:
Comme vous pouvez le constater, les deux fonctions font la même chose. La seule différence est que return_dates()
donne une liste et yield_dates()
donne un générateur.
Un exemple concret serait par exemple de lire un fichier ligne par ligne ou de créer un générateur.
yield
est comme un élément de retour pour une fonction. La différence est que l'élément yield
transforme une fonction en générateur. Un générateur se comporte comme une fonction jusqu'à ce que quelque chose soit «cédé». Le générateur s’arrête jusqu’à son prochain appel et continue exactement au même point qu’il a commencé. Vous pouvez obtenir une séquence de toutes les valeurs «produites» en une seule en appelant list(generator())
.
Le mot clé yield
recueille simplement les résultats renvoyés. Pensez à yield
comme return +=
Voici une approche simple basée sur yield
, pour calculer la série de fibonacci, a expliqué:
def fib(limit=50):
a, b = 0, 1
for i in range(limit):
yield b
a, b = b, a+b
Lorsque vous entrez ceci dans votre REPL et que vous essayez ensuite de l'appeler, vous obtenez un résultat mystificateur:
>>> fib()
<generator object fib at 0x7fa38394e3b8>
En effet, la présence de yield
signalant à Python que vous souhaitez créer un générateur, c'est-à-dire un objet qui génère des valeurs à la demande.
Alors, comment générez-vous ces valeurs? Cela peut être réalisé directement à l'aide de la fonction intégrée next
ou indirectement en l'insérant dans une construction qui consomme des valeurs.
A l'aide de la fonction next()
intégrée, vous appelez directement le .next
/__next__
, ce qui oblige le générateur à produire une valeur:
>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5
Indirectement, si vous fournissez fib
à une boucle for
, à un initialiseur list
, à un initialiseur Tuple
ou à tout autre élément qui attend un objet qui génère/produit des valeurs, vous "consommerez" le générateur jusqu'à ce qu'il ne puisse plus produire de valeurs ( et ça revient):
results = []
for i in fib(30): # consumes fib
results.append(i)
# can also be accomplished with
results = list(fib(30)) # consumes fib
De même, avec un initialiseur Tuple
:
>>> Tuple(fib(5)) # consumes fib
(1, 1, 2, 3, 5)
Un générateur diffère d'une fonction en ce sens qu'il est paresseux. Pour ce faire, il maintient son état local et vous permet de le reprendre à tout moment.
Lorsque vous appelez pour la première fois fib
en l'appelant:
f = fib()
Python compile la fonction, rencontre le mot clé yield
et renvoie simplement un objet générateur à votre retour. Pas très utile semble-t-il.
Lorsque vous lui demandez ensuite de générer la première valeur, directement ou indirectement, il exécute toutes les instructions trouvées jusqu'à ce qu'il rencontre une yield
. Il renvoie ensuite la valeur que vous avez fournie à yield
et se met en pause. Pour un exemple qui illustre mieux cela, utilisons quelques appels print
(remplacez par print "text"
si sur Python 2):
def yielder(value):
""" This is an infinite generator. Only use next on it """
while 1:
print("I'm going to generate the value for you")
print("Then I'll pause for a while")
yield value
print("Let's go through it again.")
Maintenant, entrez dans le REPL:
>>> gen = yielder("Hello, yield!")
vous avez maintenant un objet générateur en attente d'une commande pour qu'il génère une valeur. Utilisez next
et voyez ce qui est imprimé:
>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'
Les résultats non cités sont ce qui est imprimé. Le résultat cité est ce qui est retourné de yield
. Appelez next
encore une fois maintenant:
>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'
Le générateur se souvient qu'il a été mis en pause à yield value
et reprend à partir de là. Le message suivant est imprimé et la recherche de l'instruction yield
pour y suspendre est exécutée à nouveau (en raison de la boucle while
).
Un exemple simple de ce que cela explique facilement: yield
def f123():
for _ in range(4):
yield 1
yield 2
for i in f123():
print i
La sortie est:
1 2 1 2 1 2 1 2
Encore un autre TL; DR
Iterator on list: next()
renvoie l'élément suivant de la liste
Le générateur Iterator: next()
calculera le prochain élément à la volée (code d'exécution)
Vous pouvez voir le rendement/générateur comme un moyen d'exécuter manuellement le flux control de l'extérieur (comme avec la boucle continue une étape), en appelant next
, quelle que soit la complexité du flux.
Note: Le générateur est ET NON une fonction normale. Il se souvient de l'état précédent comme des variables locales (pile). Voir d'autres réponses ou articles pour une explication détaillée. Le générateur ne peut être itéré qu'une seule fois. Vous pourriez vous passer de yield
, mais ce ne serait pas aussi agréable, donc cela peut être considéré comme du sucre «très gentil».
rendement est similaire à retour. La différence est:
yield rend une fonction itérable (dans l'exemple suivant, la fonction primes(n = 1)
devient itérable).
Cela signifie essentiellement que la prochaine fois que la fonction sera appelée, elle continuera là où elle est partie (après la ligne yield expression
).
def isprime(n):
if n == 1:
return False
for x in range(2, n):
if n % x == 0:
return False
else:
return True
def primes(n = 1):
while(True):
if isprime(n): yield n
n += 1
for n in primes():
if n > 100: break
print(n)
Dans l'exemple ci-dessus, si isprime(n)
est true, le nombre premier sera renvoyé. A la prochaine itération, cela continuera à partir de la ligne suivante
n += 1
Toutes les réponses ici sont super; mais un seul d'entre eux (le plus voté) concerne comment votre code fonctionne. D'autres concernent générateurs en général et leur fonctionnement.
Je ne vais donc pas répéter ce que sont les générateurs ou les rendements; Je pense que ceux-ci sont couverts par d'excellentes réponses existantes. Cependant, après avoir passé quelques heures à essayer de comprendre un code similaire au vôtre, je vais vous expliquer comment cela fonctionne.
Votre code traverse une arborescence binaire. Prenons cet arbre par exemple:
5
/ \
3 6
/ \ \
1 4 8
Et une autre implémentation plus simple d'une traversée d'arborescence de recherche binaire:
class Node(object):
..
def __iter__(self):
if self.has_left_child():
for child in self.left:
yield child
yield self.val
if self.has_right_child():
for child in self.right:
yield child
Le code d'exécution est sur l'objet Tree
, qui implémente __iter__
comme ceci:
def __iter__(self):
class EmptyIter():
def next(self):
raise StopIteration
if self.root:
return self.root.__iter__()
return EmptyIter()
L'instruction while candidates
peut être remplacée par for element in tree
; Python traduit cela en
it = iter(TreeObj) # returns iter(self.root) which calls self.root.__iter__()
for element in it:
.. process element ..
Étant donné que la fonction Node.__iter__
est un générateur, le code à l'intérieur de celle-ci est exécuté par itération. Donc, l'exécution ressemblerait à ceci:
for
les répète (appelons-le it1 car c'est le premier objet itérateur)for
est exécutée. Le for child in self.left
crée un nouvel itérateur à partir de self.left
, qui est un objet Node lui-même (it2)iterator
est créée (it3)it3
n'a plus d'enfants, il continue et yield self.value
next(it3)
, il lève StopIteration
et existe car il n’a pas d’enfants corrects (il atteint la fin de la fonction sans rien céder)it1
et it2
sont toujours actifs - ils ne sont pas épuisés et appeler next(it2)
produirait des valeurs et non une augmentation StopIteration
it2
et appelons next(it2)
qui continue là où il s’est arrêté: juste après l’instruction yield child
. Puisqu'il n'a plus d'enfants, il continue et donne son self.val
.Le problème ici est que chaque itération crée des sous-itérateurs pour parcourir l’arbre et conserve l’état de l’itérateur actuel. Une fois qu'il a atteint la fin, il retourne dans la pile et les valeurs sont renvoyées dans le bon ordre (la plus petite valeur en premier).
Votre exemple de code a fait quelque chose de similaire dans une technique différente: il a renseigné une liste de un élément pour chaque enfant, puis à la prochaine itération, il apparaît et exécute le code de fonction sur l'objet actuel (d'où la self
).
J'espère que cela a contribué un peu à ce sujet légendaire. J'ai passé plusieurs bonnes heures à dessiner ce processus pour le comprendre.
En bref, l'utilisation de yield est similaire au mot clé return, sauf qu'elle renvoie un générateur .
Un objet generator ne parcourt que une fois.
rendement présente deux avantages:
En Python, generators
(un type spécial de iterators
) est utilisé pour générer une série de valeurs et le mot clé yield
est identique au mot clé return
des fonctions du générateur.
L’autre chose fascinante que le mot clé yield
fait est d’enregistrer la state
d’une fonction génératrice .
Ainsi, nous pouvons définir une valeur number
à une valeur différente chaque fois que la valeur generator
donne.
Voici un exemple:
def getPrimes(number):
while True:
if isPrime(number):
number = yield number # a miracle occurs here
number += 1
def printSuccessivePrimes(iterations, base=10):
primeGenerator = getPrimes(base)
primeGenerator.send(None)
for power in range(iterations):
print(primeGenerator.send(base ** power))
>>> def create_generator():
... my_list = range(3)
... for i in my_list:
... yield i*i
...
>>> my_generator = create_generator() # create a generator
>>> print(my_generator) # my_generator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in my_generator:
... print(i)
0
1
4
En bref , vous pouvez voir que la boucle ne s'arrête pas et continue de fonctionner même après l'envoi de l'objet ou de la variable (contrairement à return
où la boucle s'arrête après l'exécution).
Une analogie pourrait aider à saisir l'idée ici:
Imaginez que vous ayez créé une machine étonnante capable de générer des milliers et des milliers d'ampoules par jour. La machine génère ces ampoules dans des boîtes avec un numéro de série unique. Vous ne disposez pas de suffisamment d'espace pour stocker toutes ces ampoules en même temps (par exemple, vous ne pouvez pas suivre la vitesse de la machine en raison d'une limitation de stockage), vous souhaitez donc ajuster cette machine pour générer des ampoules à la demande.
Les générateurs Python ne diffèrent pas beaucoup de ce concept.
Imaginez que vous ayez une fonction x
qui génère des numéros de série uniques pour les boîtes. De toute évidence, vous pouvez avoir un très grand nombre de codes à barres de ce type générés par la fonction. Une option plus sage et plus efficace consiste à générer ces numéros de série à la demande.
Code de la machine:
def barcode_generator():
serial_number = 10000 # Initial barcode
while True:
yield serial_number
serial_number += 1
barcode = barcode_generator()
while True:
number_of_lightbulbs_to_generate = int(input("How many lightbulbs to generate? "))
barcodes = [next(barcode) for _ in range(number_of_lightbulbs_to_generate)]
print(barcodes)
# function_to_create_the_next_batch_of_lightbulbs(barcodes)
produce_more = input("Produce more? [Y/n]: ")
if produce_more == "n":
break
Comme vous pouvez le constater, une "fonction" autonome permet de générer chaque fois le numéro de série unique suivant. Cette fonction retourne un générateur! Comme vous pouvez le constater, nous n’appelons pas la fonction à chaque fois qu’il nous faut un nouveau numéro de série, mais nous utilisons next()
étant donné que le générateur génère le prochain numéro de série.
Sortie:
How many lightbulbs to generate? 5
[10000, 10001, 10002, 10003, 10004]
Produce more? [Y/n]: y
How many lightbulbs to generate? 6
[10005, 10006, 10007, 10008, 10009, 10010]
Produce more? [Y/n]: y
How many lightbulbs to generate? 7
[10011, 10012, 10013, 10014, 10015, 10016, 10017]
Produce more? [Y/n]: n
yield
Est un type de générateur pouvant être utilisé en python.
voici un lien pour voir ce que Yield fait vraiment, également en génération . Moteurs générateurs & Yield - Python Central (PC)
De plus, yield
fonctionne comme return
, mais d'une manière différente de return
. Même s'il existe un lien qui explique yield
davantage, si vous ne comprenez pas l'autre. Améliorez vos compétences de rendement - jeffknupp
En termes simples, «rendement» est similaire à «renvoyer» une valeur, mais cela fonctionne avec Generator.
En simple rendement, retourne l'objet générateur au lieu de valeurs.
Ci-dessous, un exemple simple aidera!
def sim_generator():
for i in range(3):
yield(i)
obj = sim_generator()
next(obj) # another way is obj.__next__()
next(obj)
next(obj)
le code ci-dessus renvoie 0, 1, 2
ou même court
for val in sim_generator():
print(val)
retourne 0, 1, 2
J'espère que cela t'aides
Une fonction de générateur simple
def my_gen():
n = 1
print('This is printed first')
# Generator function contains yield statements
yield n
n += 1
print('This is printed second')
yield n
n += 1
print('This is printed at last')
yield n
l'instruction de rendement met la fonction en pause, en enregistrant tous ses états, puis se poursuit lors d'appels successifs.
yield
donne quelque chose. C'est comme si quelqu'un vous demandait de faire 5 petits gâteaux. Si vous avez terminé avec au moins un gâteau, vous pouvez le leur donner à manger pendant que vous faites d'autres gâteaux.
In [4]: def make_cake(numbers):
...: for i in range(numbers):
...: yield 'Cake {}'.format(i)
...:
In [5]: factory = make_cake(5)
Ici factory
s'appelle générateur, ce qui vous fait des gâteaux. Si vous appelez make_function
, vous obtenez un générateur au lieu d’exécuter cette fonction. C'est parce que lorsque le mot clé yield
est présent dans une fonction, il devient un générateur.
In [7]: next(factory)
Out[7]: 'Cake 0'
In [8]: next(factory)
Out[8]: 'Cake 1'
In [9]: next(factory)
Out[9]: 'Cake 2'
In [10]: next(factory)
Out[10]: 'Cake 3'
In [11]: next(factory)
Out[11]: 'Cake 4'
Ils ont consommé tous les gâteaux, mais ils en redemandent un.
In [12]: next(factory)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-12-0f5c45da9774> in <module>
----> 1 next(factory)
StopIteration:
et on leur dit d'arrêter de demander plus. Donc, une fois que vous avez consommé un générateur, vous en avez fini. Vous avez besoin d'appeler à nouveau make_cake
si vous voulez plus de gâteaux. C'est comme passer une autre commande pour des gâteaux.
In [13]: factory = make_cake(3)
In [14]: for cake in factory:
...: print(cake)
...:
Cake 0
Cake 1
Cake 2
Vous pouvez également utiliser pour la boucle avec un générateur comme celui ci-dessus.
Un autre exemple: disons que vous voulez un mot de passe aléatoire chaque fois que vous le demandez.
In [22]: import random
In [23]: import string
In [24]: def random_password_generator():
...: while True:
...: yield ''.join([random.choice(string.ascii_letters) for _ in range(8)])
...:
In [25]: rpg = random_password_generator()
In [26]: for i in range(3):
...: print(next(rpg))
...:
FXpUBhhH
DdUDHoHn
dvtebEqG
In [27]: next(rpg)
Out[27]: 'mJbYRMNo'
Ici rpg
est un générateur qui peut générer un nombre infini de mots de passe aléatoires. Nous pouvons donc aussi dire que les générateurs sont utiles lorsque nous ne connaissons pas la longueur de la séquence, contrairement à la liste qui contient un nombre fini d'éléments.