le but de la fonction «envoyer» du générateur python?

165

Quelqu'un peut-il me donner un exemple de la raison pour laquelle la fonction «envoyer» associée à la fonction de générateur Python existe? Je comprends parfaitement la fonction de rendement. Cependant, la fonction d'envoi est déroutante pour moi. La documentation sur cette méthode est compliquée:

generator.send(value)

Reprend l'exécution et «envoie» une valeur dans la fonction générateur. L'argument value devient le résultat de l'expression de rendement actuelle. La méthode send () renvoie la valeur suivante fournie par le générateur, ou lève StopIteration si le générateur se ferme sans donner une autre valeur.

Qu'est-ce que ça veut dire? Je pensais que la valeur était l'entrée de la fonction? La phrase "La méthode send () renvoie la valeur suivante fournie par le générateur" semble être aussi le but exact de la fonction yield; yield renvoie la valeur suivante fournie par le générateur ...

Quelqu'un peut-il me donner un exemple d'un générateur utilisant l'envoi qui accomplit quelque chose que le rendement ne peut pas?

Tommy
la source
3
dupliquer: stackoverflow.com/questions/12637768/…
Bas Swinckels
3
Ajout d'un autre exemple réel (lecture à partir de FTP) lorsque les rappels sont transformés en générateur utilisé de l'intérieur
Jan Vlcinsky
2
Il convient de mentionner que "Quand send()est appelé pour démarrer le générateur, il doit être appelé avec Nonecomme argument, car il n'y a pas d'expression yield qui pourrait recevoir la valeur.", Cité dans la documentation officielle et pour laquelle la citation dans la question est manquant.
Rick le

Réponses:

147

Il est utilisé pour envoyer des valeurs dans un générateur qui vient de produire. Voici un exemple explicatif artificiel (non utile):

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> gen = double_inputs()
>>> next(gen)       # run up to the first yield
>>> gen.send(10)    # goes into 'x' variable
20
>>> next(gen)       # run up to the next yield
>>> gen.send(6)     # goes into 'x' again
12
>>> next(gen)       # run up to the next yield
>>> gen.send(94.3)  # goes into 'x' again
188.5999999999999

Vous ne pouvez pas faire ça uniquement avec yield.

Quant à savoir pourquoi il est utile, l'un des meilleurs cas d'utilisation que j'ai vus est celui de Twisted @defer.inlineCallbacks. Essentiellement, cela vous permet d'écrire une fonction comme celle-ci:

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)

Ce qui se passe, c'est que takesTwoSeconds()renvoie a Deferred, qui est une valeur promettant qu'une valeur sera calculée plus tard. Twisted peut exécuter le calcul dans un autre thread. Lorsque le calcul est terminé, il le passe dans le différé, et la valeur est ensuite renvoyée à la doStuff()fonction. Ainsi, le doStuff()peut finir par ressembler plus ou moins à une fonction procédurale normale, sauf qu'il peut faire toutes sortes de calculs et de rappels, etc. L'alternative avant cette fonctionnalité serait de faire quelque chose comme:

def doStuff():
    returnDeferred = defer.Deferred()
    def gotNextResult(nextResult):
        returnDeferred.callback(nextResult / 10)
    def gotResult(result):
        takesTenSeconds(result * 10).addCallback(gotNextResult)
    takesTwoSeconds().addCallback(gotResult)
    return returnDeferred

C'est beaucoup plus compliqué et compliqué.

Claudiu
la source
2
Pouvez-vous expliquer quel en est le but? Pourquoi cela ne peut-il pas être recréé avec double_inputs (startingnumber) et yield?
Tommy
@Tommy: oh parce que les valeurs que vous avez n'ont rien à voir avec les précédentes. laissez-moi changer l'exemple
Claudiu
pourquoi l'utiliseriez-vous alors sur une fonction simple qui double son entrée ??
Tommy
4
@Tommy: Vous ne le feriez pas. Le premier exemple consiste simplement à expliquer ce qu'il fait. Le deuxième exemple concerne un cas d'utilisation réellement utile.
Claudiu
1
@Tommy: Je dirais que si vous voulez vraiment savoir, regardez cette présentation et travaillez à travers tout cela. Une réponse courte ne suffira pas, car alors vous direz simplement "Mais est-ce que je ne peux pas le faire comme ça?" etc.
Claudiu
96

Cette fonction consiste à écrire des coroutines

def coroutine():
    for i in range(1, 10):
        print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
    while True:
        print("From user {}".format(c.send(1)))
except StopIteration: pass

impressions

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...

Voyez comment le contrôle est passé dans les deux sens? Ce sont des coroutines. Ils peuvent être utilisés pour toutes sortes de choses intéressantes comme asynch IO et autres.

Pensez-y comme ça, avec un générateur et pas d'envoi, c'est une rue à sens unique

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

Mais avec envoyer, ça devient une rue à double sens

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

Ce qui ouvre la porte à l'utilisateur en personnalisant le comportement des générateurs à la volée et au générateur répondant à l'utilisateur.

Daniel Gratzer
la source
3
mais une fonction de générateur peut prendre des paramètres. Comment "Send" va-t-il au-delà de l'envoi d'un paramètre au générateur?
Tommy
13
@Tommy Parce que vous ne pouvez pas changer les paramètres d'un générateur pendant qu'il fonctionne. Vous lui donnez des paramètres, il s'exécute, c'est fait. Avec send, vous lui donnez des paramètres, il tourne un peu, vous lui envoyez une valeur et il fait quelque chose de différent, répétez
Daniel Gratzer
2
@Tommy Cela redémarrera le générateur, ce qui vous obligera à refaire beaucoup de travail
Daniel Gratzer
5
Pourriez-vous s'il vous plaît expliquer le but de l'envoi d'un Aucun avant tout?
Shubham Aggarwal
2
@ShubhamAggarwal C'est fait pour «démarrer» le générateur. C'est juste quelque chose qui doit être fait. Cela a du sens quand vous y réfléchissez car la première fois que vous appelez send()le générateur n'a pas encore atteint le mot-clé yield.
Michael
50

Cela peut aider quelqu'un. Voici un générateur qui n'est pas affecté par la fonction d'envoi. Il prend le paramètre number lors de l'instanciation et n'est pas affecté par send:

>>> def double_number(number):
...     while True:
...         number *=2 
...         yield number
... 
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256

Maintenant, voici comment vous feriez le même type de fonction en utilisant send, donc à chaque itération, vous pouvez changer la valeur de number:

def double_number(number):
    while True:
        number *= 2
        number = yield number

Voici à quoi cela ressemble, comme vous pouvez le voir, l'envoi d'une nouvelle valeur pour nombre modifie le résultat:

>>> def double_number(number):
...     while True:
...         number *= 2
...         number = yield number
...
>>> c = double_number(4)
>>> 
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6

Vous pouvez également mettre ceci dans une boucle for en tant que telle:

for x in range(10):
    n = c.send(n)
    print n

Pour plus d'aide, consultez cet excellent tutoriel .

Radtek
la source
12
Cette comparaison entre une fonction qui n'est pas affectée par send () et une qui le fait, a vraiment aidé. Merci!
Manas Bajaj
Comment cela peut-il être un exemple illustratif de l'objectif de send? Un simple lambda x: x * 2fait la même chose d'une manière beaucoup moins compliquée.
user209974
Utilise-t-il envoyer? Allez et ajoutez votre réponse.
radtek
17

Quelques cas d'utilisation pour l'utilisation du générateur et send()

Générateurs avec send()allow:

  • se souvenir de l'état interne de l'exécution
    • à quelle étape nous en sommes
    • quel est l'état actuel de nos données
  • retour d'une séquence de valeurs
  • réception d'une séquence d'entrées

Voici quelques cas d'utilisation:

Regardé tentative de suivre une recette

Laissez-nous avoir une recette, qui attend un ensemble prédéfini d'entrées dans un certain ordre.

Nous pouvons:

  • créer une watched_attemptinstance à partir de la recette
  • laissez-le obtenir quelques entrées
  • avec chaque entrée renvoie des informations sur ce qui est actuellement dans le pot
  • à chaque entrée vérifier, que l'entrée est celle attendue (et échouer si ce n'est pas le cas)

    def recipe():
        pot = []
        action = yield pot
        assert action == ("add", "water")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("add", "salt")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("boil", "water")
    
        action = yield pot
        assert action == ("add", "pasta")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("decant", "water")
        pot.remove("water")
    
        action = yield pot
        assert action == ("serve")
        pot = []
        yield pot
    

Pour l'utiliser, créez d'abord l' watched_attemptinstance:

>>> watched_attempt = recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     

L'appel à .next()est nécessaire pour démarrer l'exécution du générateur.

La valeur retournée montre, notre pot est actuellement vide.

Maintenant, faites quelques actions en suivant ce que la recette attend:

>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "salt"))                                                                      
['water', 'salt']                                                                                      
>>> watched_attempt.send(("boil", "water"))                                                                    
['water', 'salt']                                                                                      
>>> watched_attempt.send(("add", "pasta"))                                                                     
['water', 'salt', 'pasta']                                                                             
>>> watched_attempt.send(("decant", "water"))                                                                  
['salt', 'pasta']                                                                                      
>>> watched_attempt.send(("serve"))                                                                            
[] 

Comme on le voit, le pot est enfin vide.

Dans le cas où on ne suivrait pas la recette, elle échouerait (quel résultat escompté d'une tentative surveillée de cuisiner quelque chose - juste en apprenant que nous n'avons pas prêté suffisamment d'attention lorsqu'on leur a donné des instructions.

>>> watched_attempt = running.recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     
>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "pasta")) 

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))

/home/javl/sandbox/stack/send/running.py in recipe()
     29
     30     action = yield pot
---> 31     assert action == ("add", "salt")
     32     pot.append(action[1])
     33

AssertionError:

Remarquerez que:

  • il y a une séquence linéaire d'étapes attendues
  • les étapes peuvent différer (certaines s'enlèvent, d'autres s'ajoutent au pot)
  • nous réussissons à faire tout cela par un générateur de fonctions - pas besoin d'utiliser une classe complexe ou des structures similaires.

Totaux courants

Nous pouvons utiliser le générateur pour suivre le total des valeurs qui lui sont envoyées.

Chaque fois que nous ajoutons un nombre, le nombre d'entrées et la somme totale sont renvoyés (valable pour le moment où l'entrée précédente y a été envoyée).

from collections import namedtuple

RunningTotal = namedtuple("RunningTotal", ["n", "total"])


def runningtotals(n=0, total=0):
    while True:
        delta = yield RunningTotal(n, total)
        if delta:
            n += 1
            total += delta


if __name__ == "__main__":
    nums = [9, 8, None, 3, 4, 2, 1]

    bookeeper = runningtotals()
    print bookeeper.next()
    for num in nums:
        print num, bookeeper.send(num)

La sortie ressemblerait à:

RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)
Jan Vlcinsky
la source
3
Je lance votre exemple et en python 3 semble que le watch_attempt.next () doit être remplacé par next (watch_attempt).
thanos.a
15

La send()méthode contrôle la valeur à gauche de l'expression de rendement.

Pour comprendre en quoi le rendement diffère et quelle valeur il détient, actualisons d'abord rapidement l'ordre dans lequel le code python est évalué.

Section 6.15 Ordre d'évaluation

Python évalue les expressions de gauche à droite. Notez que lors de l'évaluation d'une affectation, le côté droit est évalué avant le côté gauche.

Ainsi, une expression a = bdu côté droit est évaluée en premier.

Comme le montre ce qui suit, a[p('left')] = p('right')le côté droit est évalué en premier.

>>> def p(side):
...     print(side)
...     return 0
... 
>>> a[p('left')] = p('right')
right
left
>>> 
>>> 
>>> [p('left'), p('right')]
left
right
[0, 0]

Que fait yield ?, cède, suspend l'exécution de la fonction et retourne à l'appelant, et reprend l'exécution au même endroit qu'elle avait laissé avant la suspension.

Où exactement l'exécution est-elle suspendue? Vous l'avez peut-être déjà deviné ... l' exécution est suspendue entre le côté droit et gauche de l'expression yield. Ainsi, new_val = yield old_vall'exécution est interrompue au =signe, et la valeur à droite (qui est avant la suspension, et est également la valeur renvoyée à l'appelant) peut être quelque chose de différent de la valeur à gauche (qui est la valeur attribuée après la reprise exécution).

yield donne 2 valeurs, une à droite et une autre à gauche.

Comment contrôlez-vous la valeur à gauche de l'expression de rendement? via la .send()méthode.

6.2.9. Expressions de rendement

La valeur de l'expression yield après la reprise dépend de la méthode qui a repris l'exécution. Si __next__()est utilisé (généralement via un for ou le next()builtin), le résultat est None. Sinon, si send()est utilisé, le résultat sera la valeur transmise à cette méthode.

user2059857
la source
13

La sendméthode implémente des coroutines .

Si vous n'avez pas rencontré Coroutines, ils sont difficiles à comprendre car ils changent la façon dont un programme se déroule. Vous pouvez lire un bon tutoriel pour plus de détails.

Jochen Ritzel
la source
6

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:

  1. Supposons que ce gsoit le générateur. g.sendpousse une valeur vers la gauche à travers l'extrémité droite du tuyau.
  2. Exécution dans le cadre de gpauses, permettant au corps de la fonction génératrice de s'exécuter.
  3. 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.
  4. L'exécution se poursuit dans le corps de la fonction génératrice jusqu'à ce que la ligne suivante contenant yieldsoit atteinte.
  5. 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.
  6. 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.
  7. g.send reprend et extrait la valeur et la renvoie à l'utilisateur.
  8. 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 :

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. 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:

  1. Nous avons déplacé l'implémentation dans une fonction imbriquée.
  2. 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.
  3. 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.
  4. 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().
  5. 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é.

Stylo à billeBen
la source
"L'argument 1 dans g = f (1) a été capturé normalement et assigné à y dans le corps de f, mais le while True n'a pas encore commencé." Pourquoi pas? Pourquoi Python n'essaierait-il pas d'exécuter ce code jusqu'à ce qu'il rencontre par exemple yield?
Josh
@Josh Le curseur n'est pas avancé jusqu'au premier appel à send; il faut un appel de send(None)pour déplacer le curseur vers la première yieldinstruction, et alors seulement les sendappels suivants envoient réellement une valeur "réelle" à yield.
BallpointBen
Merci - C'est intéressant, donc l'interprète sait que la fonction f sera yield à un moment donné, et attendra donc qu'elle reçoive un sendde l'appelant? Avec une fonction normale cal, l'interpréteur commencerait tout de suite à s'exécuter f, non? Après tout, il n'y a aucune compilation AOT d'aucune sorte en Python. Etes-vous sûr que c'est le cas? (sans remettre en question ce que vous dites, je suis vraiment perplexe par ce que vous avez écrit ici). Où puis-je en savoir plus sur la façon dont Python sait qu'il doit attendre avant de commencer à exécuter le reste de la fonction?
Josh
@Josh J'ai construit ce modèle mental simplement en observant le fonctionnement des différents générateurs de jouets, sans aucune compréhension des composants internes de Python. Cependant, le fait que l'initial send(None)donne la valeur appropriée (par exemple 1) sans envoyer Nonedans le générateur suggère que le premier appel à sendest un cas particulier. C'est une interface délicate à concevoir; si vous laissez le premier sendenvoyer une valeur arbitraire, alors l'ordre des valeurs produites et des valeurs envoyées serait décalé de un par rapport à ce qu'il est actuellement.
BallpointBen
Merci BallpointBen. Très intéressant, j'ai laissé une question ici pour voir pourquoi c'est le cas.
Josh
2

Celles-ci me confondaient aussi. Voici un exemple que j'ai fait en essayant de mettre en place un générateur qui cède et accepte des signaux dans un ordre alterné (rendement, acceptation, rendement, acceptation) ...

def echo_sound():

    thing_to_say = '<Sound of wind on cliffs>'
    while True:
        thing_to_say = (yield thing_to_say)
        thing_to_say = '...'.join([thing_to_say]+[thing_to_say[-6:]]*2)
        yield None  # This is the return value of send.

gen = echo_sound()

print 'You are lost in the wilderness, calling for help.'

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Hello!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Is anybody out there?'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Help!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

La sortie est:

You are lost in the wilderness, calling for help.
------
You hear: "<Sound of wind on cliffs>"
You yell "Hello!"
------
You hear: "Hello!...Hello!...Hello!"
You yell "Is anybody out there?"
------
You hear: "Is anybody out there?...there?...there?"
You yell "Help!"
Peter
la source