Le mot «céder» a deux significations: produire quelque chose (par exemple, donner du maïs) et s'arrêter pour laisser quelqu'un / autre continuer (par exemple, des voitures cédant aux piétons). Les deux définitions s'appliquent au yieldmot - clé de Python ; ce qui rend les fonctions génératrices spéciales, c'est que contrairement aux fonctions régulières, les valeurs peuvent être "retournées" à l'appelant tout en mettant simplement en pause, et non en terminant, une fonction génératrice.
Il est plus facile d'imaginer un générateur comme une extrémité d'un tuyau bidirectionnel avec une extrémité «gauche» et une extrémité «droite»; ce tuyau est le support sur lequel les valeurs sont envoyées entre le générateur lui-même et le corps de la fonction générateur. Chaque extrémité du tube comporte deux opérations push:, qui envoie une valeur et bloque jusqu'à ce que l'autre extrémité du tube tire la valeur et ne renvoie rien; etpull, qui bloque jusqu'à ce que l'autre extrémité du tube pousse une valeur et renvoie la valeur poussée. Au moment de l'exécution, l'exécution rebondit entre les contextes de chaque côté du tube - chaque côté s'exécute jusqu'à ce qu'il envoie une valeur à l'autre côté, à quel point il s'arrête, laisse l'autre côté s'exécuter et attend une valeur dans retour, à quel point l'autre côté s'arrête et il reprend. En d'autres termes, chaque extrémité du tuyau s'étend du moment où il reçoit une valeur au moment où il envoie une valeur.
Le tuyau est fonctionnellement symétrique, mais - par convention que je définis dans cette réponse - l'extrémité gauche n'est disponible qu'à l'intérieur du corps de la fonction du générateur et est accessible via le yieldmot - clé, tandis que l'extrémité droite est le générateur et est accessible via le sendfonction du générateur . Comme interfaces singulières à leurs extrémités respectives du tube, yieldet une senddouble fonction: ils ont chacun deux poussoirs et les valeurs de traction vers / à partir de leurs extrémités de la conduite, yieldpoussant vers la droite et vers la gauche tout en tirant sendfait le contraire. Ce double devoir est au cœur de la confusion entourant la sémantique des déclarations comme x = yield y. Briser yieldet senden deux push / pull étapes explicites fera leur sémantique beaucoup plus claire:
- Supposons que ce
gsoit le générateur. g.sendpousse une valeur vers la gauche à travers l'extrémité droite du tuyau.
- Exécution dans le cadre de
gpauses, permettant au corps de la fonction génératrice de s'exécuter.
- La valeur poussée par
g.sendest tirée vers la gauche yieldet reçue à l'extrémité gauche du tuyau. In x = yield y, xest affecté à la valeur extraite.
- L'exécution se poursuit dans le corps de la fonction génératrice jusqu'à ce que la ligne suivante contenant
yieldsoit atteinte.
yieldpousse une valeur vers la droite à travers l'extrémité gauche du tuyau, vers le haut g.send. Dans x = yield y, yest poussé vers la droite à travers le tuyau.
- L'exécution dans le corps de la fonction génératrice s'interrompt, permettant à la portée externe de continuer là où elle s'était arrêtée.
g.send reprend et extrait la valeur et la renvoie à l'utilisateur.
- Lors du
g.sendprochain appel, revenez à l'étape 1.
Bien que cyclique, cette procédure a un début: quand g.send(None)- ce qui next(g)est l'abréviation de - est appelée pour la première fois (il est interdit de passer autre chose qu'au Nonepremier sendappel). Et cela peut avoir une fin: quand il n'y a plus d' yieldinstructions à atteindre dans le corps de la fonction génératrice.
Voyez-vous ce qui rend la yielddéclaration (ou plus précisément les générateurs) si spéciale? Contrairement au returnmot clé maigre , yieldest capable de transmettre des valeurs à son appelant et de recevoir des valeurs de son appelant sans mettre fin à la fonction dans laquelle il vit! (Bien sûr, si vous souhaitez terminer une fonction - ou un générateur - il est également pratique d'avoir le returnmot - clé.) Lorsqu'une yieldinstruction est rencontrée, la fonction de générateur se met simplement en pause, puis reprend là où elle était off lors de l'envoi d'une autre valeur. Et sendc'est juste l'interface pour communiquer avec l'intérieur d'une fonction de générateur depuis l'extérieur.
Si nous voulons vraiment casser cette analogie push / pull / pipe aussi loin que possible, nous nous retrouvons avec le pseudo-code suivant qui nous ramène vraiment à la maison, mis à part les étapes 1 à 5, yieldet qui sendsont les deux côtés du même tube de pièces :
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
La transformation clé est que nous avons divisé x = yield yet value1 = g.send(value2)chacun en deux déclarations: left_end.push(y)et x = left_end.pull(); et value1 = right_end.pull()et right_end.push(value2). Il existe deux cas particuliers du yieldmot - clé: x = yieldet yield y. Ce sont des sucres syntaxiques, respectivement, pour x = yield Noneet _ = yield y # discarding value.
Pour plus de détails sur l'ordre précis dans lequel les valeurs sont envoyées via le canal, voir ci-dessous.
Ce qui suit est un modèle concret assez long de ce qui précède. Tout d'abord, il convient de noter que pour tout générateur g, next(g)est exactement équivalent à g.send(None). Dans cet esprit, nous pouvons nous concentrer uniquement sur la façon dont sendfonctionne et parler uniquement de l'avancement du générateur avec send.
Supposons que nous ayons
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
Maintenant, la définition de fgrossièrement desugars à la fonction ordinaire (non génératrice) suivante:
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
Ce qui suit s'est produit dans cette transformation de f:
- Nous avons déplacé l'implémentation dans une fonction imbriquée.
- Nous avons créé un tube bidirectionnel
left_endauquel la fonction imbriquée accédera et dont right_endla portée externe sera renvoyée et accédée - right_endc'est ce que nous appelons l'objet générateur.
- Dans la fonction imbriquée, la toute première chose que nous faisons est de vérifier que
left_end.pull()c'est Noneconsommer une valeur poussée dans le processus.
- Dans la fonction imbriquée, l'instruction
x = yield ya été remplacée par deux lignes: left_end.push(y)et x = left_end.pull().
- Nous avons défini la
sendfonction pour right_end, qui est la contrepartie des deux lignes par lesquelles nous avons remplacé l' x = yield yinstruction à l'étape précédente.
Dans ce monde fantastique où les fonctions peuvent continuer après le retour, gest assigné right_endpuis impl()appelé. Donc, dans notre exemple ci-dessus, si nous suivions l'exécution ligne par ligne, ce qui se passerait est à peu près le suivant:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
Cela correspond exactement au pseudocode en 16 étapes ci-dessus.
Il y a d'autres détails, comme la façon dont les erreurs se propagent et ce qui se passe lorsque vous atteignez la fin du générateur (le tuyau est fermé), mais cela devrait clarifier le fonctionnement du flux de contrôle de base lorsqu'il sendest utilisé.
En utilisant ces mêmes règles de désuétude, examinons deux cas particuliers:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
Pour la plupart, ils désugarent de la même manière que f, les seules différences sont la façon dont les yieldinstructions sont transformées:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
Dans le premier, la valeur transmise à f1est poussée (renvoyée) initialement, puis toutes les valeurs extraites (envoyées) sont repoussées (renvoyées) vers l'arrière. Dans le second, xn'a pas (encore) de valeur quand il arrive pour la première fois push, donc an UnboundLocalErrorest élevé.