Przeznaczenie funkcji „wyślij” generatora Pythona?

164

Czy ktoś może mi podać przykład, dlaczego istnieje funkcja „wyślij” związana z funkcją generatora Pythona? W pełni rozumiem funkcję wydajności. Jednak funkcja wysyłania jest dla mnie myląca. Dokumentacja dotycząca tej metody jest skomplikowana:

generator.send(value)

Wznawia wykonywanie i „wysyła” wartość do funkcji generatora. Argument wartość staje się wynikiem bieżącego wyrażenia zysku. Metoda send () zwraca następną wartość uzyskaną przez generator lub podnosi StopIteration, jeśli generator zakończy działanie bez zwracania innej wartości.

Co to znaczy? Myślałem, że wartość była wejściem do funkcji? Fraza „Metoda send () zwraca następną wartość uzyskaną przez generator” wydaje się być również dokładnym celem funkcji yield; yield zwraca następną wartość uzyskaną przez generator ...

Czy ktoś może mi podać przykład generatora wykorzystującego wysyłanie, który osiąga coś, czego nie da się osiągnąć?

Tommy
źródło
3
duplicate: stackoverflow.com/questions/12637768/ ...
Bas Swinckels
3
Dodano kolejny przykład z życia
wzięty
2
Warto wspomnieć, że „Kiedy send()jest wywoływany do uruchomienia generatora, musi być wywołany Nonejako argument, ponieważ nie ma wyrażenia wydajności, które mogłoby przyjąć wartość.”, Cytowane z oficjalnego dokumentu i dla którego cytat w pytaniu jest brakujący.
Rick

Odpowiedzi:

147

Służy do wysyłania wartości do generatora, który właśnie wygenerował. Oto sztuczny (nieużyteczny) przykład wyjaśniający:

>>> 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

Nie możesz tego zrobić tylko z yield.

Jeśli chodzi o to, dlaczego jest to przydatne, jednym z najlepszych przypadków użycia, jakie widziałem, jest Twisted @defer.inlineCallbacks. Zasadniczo pozwala napisać taką funkcję:

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

Dzieje się tak, że takesTwoSeconds()zwraca a Deferred, która jest wartością obiecującą, że zostanie obliczona później. Twisted może uruchomić obliczenia w innym wątku. Po zakończeniu obliczeń przekazuje je do odroczonego, a wartość jest następnie wysyłana z powrotem do doStuff()funkcji. W doStuff()rezultacie może wyglądać mniej więcej jak normalna funkcja proceduralna, z wyjątkiem tego, że może wykonywać różnego rodzaju obliczenia i wywołania zwrotne itp. Alternatywą przed tą funkcją byłoby zrobienie czegoś takiego:

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

Jest o wiele bardziej zagmatwany i nieporęczny.

Claudiu
źródło
2
Czy możesz wyjaśnić, jaki jest tego cel? Dlaczego nie można tego odtworzyć za pomocą double_inputs (numer początkowy) i wydajności?
Tommy
@Tommy: och, ponieważ wartości, które masz, nie mają nic wspólnego z poprzednią. zmienię przykład
Claudiu
dlaczego miałbyś używać tego zamiast prostej funkcji, która podwaja jej dane wejściowe?
Tommy
4
@Tommy: Nie zrobiłbyś tego. Pierwszy przykład to tylko wyjaśnienie, co robi. Drugi przykład dotyczy rzeczywiście użytecznego przypadku użycia.
Claudiu
1
@Tommy: Powiedziałbym, że jeśli naprawdę chcesz wiedzieć, sprawdź tę prezentację i popracuj nad tym wszystkim. Krótka odpowiedź nie wystarczy, ponieważ wtedy po prostu powiesz „Ale czy nie mogę po prostu zrobić tego w ten sposób?” itd.
Claudiu
96

Ta funkcja służy do pisania programów

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

wydruki

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

Widzisz, jak kontrola jest przekazywana w tę iz powrotem? To są programy. Mogą być używane do wszelkiego rodzaju fajnych rzeczy, takich jak asynchroniczne IO i podobne.

Pomyśl o tym w ten sposób, z generatorem i bez wysyłania, to ulica jednokierunkowa

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

Ale po wysłaniu staje się ulicą dwukierunkową

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

To otwiera drzwi dla użytkownika, dostosowując zachowanie generatorów w locie i generator reagujący na użytkownika.

Daniel Gratzer
źródło
3
ale funkcja generatora może przyjmować parametry. W jaki sposób „Send” wykracza poza przesłanie parametru do generatora?
Tommy
13
@Tommy Ponieważ nie możesz zmienić parametrów na generator podczas jego działania. Podajesz mu parametry, działa, gotowe. Z send nadajesz mu parametry, trochę działa, wysyłasz wartość i robi coś innego, powtórz
Daniel Gratzer
2
@Tommy Spowoduje to ponowne uruchomienie generatora, co spowoduje ponowne wykonanie dużej ilości pracy
Daniel Gratzer
5
Czy mógłbyś wyjaśnić cel wysłania None przed wszystkim?
Shubham Aggarwal
2
@ShubhamAggarwal Służy do „uruchomienia” generatora. To po prostu coś, co trzeba zrobić. Ma to sens, gdy się nad tym zastanowisz, ponieważ przy pierwszym wywołaniu send()generatora nie osiągnął yieldjeszcze słowa kluczowego .
Michael
50

To może komuś pomóc. Oto generator, na który funkcja wysyłania nie ma wpływu. Pobiera parametr liczbowy podczas tworzenia instancji i nie ma na niego wpływu wysyłanie:

>>> 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

Oto jak wykonałbyś ten sam typ funkcji za pomocą wysyłania, więc w każdej iteracji możesz zmienić wartość liczby:

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

Oto jak to wygląda, ponieważ możesz zobaczyć, że wysłanie nowej wartości liczby zmienia wynik:

>>> 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

Możesz również umieścić to w pętli for jako takiej:

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

Więcej pomocy znajdziesz w tym świetnym samouczku .

radtek
źródło
12
To porównanie funkcji, na którą send () nie ma wpływu, z tą, na którą działa, naprawdę pomogło. Dzięki!
Manas Bajaj,
Jak to może być ilustracyjnym przykładem celu send? Prosty lambda x: x * 2robi to samo w znacznie mniej zawiły sposób.
user209974
Czy używa wysyłania? Idź i dodaj swoją odpowiedź.
radtek
17

Niektóre przypadki użycia generatora i send()

Generatory z możliwością send():

  • zapamiętywanie stanu wewnętrznego wykonania
    • na jakim etapie jesteśmy
    • jaki jest aktualny stan naszych danych
  • zwracanie sekwencji wartości
  • odbieranie sekwencji wejść

Oto kilka przypadków użycia:

Oglądałem próbę przestrzegania przepisu

Miejmy przepis, który oczekuje predefiniowanego zestawu danych wejściowych w określonej kolejności.

Możemy:

  • utwórz watched_attemptinstancję z przepisu
  • niech dostanie jakieś dane wejściowe
  • z każdym wejściem zwraca informację o tym, co aktualnie znajduje się w puli
  • przy każdym sprawdzaniu wejścia, czy wejście jest oczekiwane (i kończy się niepowodzeniem, jeśli nie jest)

    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

Aby z niego skorzystać, najpierw utwórz watched_attemptinstancję:

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

Wywołanie .next()jest konieczne, aby rozpocząć wykonywanie generatora.

Zwracana wartość pokazuje, nasza pula jest obecnie pusta.

Teraz wykonaj kilka czynności zgodnie z oczekiwaniami przepisu:

>>> 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"))                                                                            
[] 

Jak widzimy, garnek jest wreszcie pusty.

W przypadku, gdyby ktoś nie zastosował się do przepisu, to się nie udał (co mogłoby być efektem obserwowanej próby ugotowania czegoś - po prostu ucząc się, że nie zwracaliśmy wystarczającej uwagi na instrukcje.

>>> 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:

Zauważ, że:

  • istnieje liniowa sekwencja oczekiwanych kroków
  • kroki mogą się różnić (niektóre usuwają, niektóre dodają do puli)
  • udaje nam się to wszystko robić za pomocą funkcji / generatora - nie ma potrzeby używania złożonych klas lub podobnych struktur.

Sumy bieżące

Możemy użyć generatora do śledzenia bieżącej sumy wysłanych do niego wartości.

Za każdym razem, gdy dodajemy liczbę, liczbę wejść i sumę sumaryczną (ważne w momencie przesłania poprzedniego wejścia).

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)

Wynik wyglądałby następująco:

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
źródło
3
Uruchamiam twój przykład i w pythonie 3 wydaje się, że watched_attempt.next () musi zostać zastąpiony przez next (watched_attempt).
thanos. A
15

Te send()kontrole metoda, jaka jest wartość po lewej stronie wyrażenia wydajność będzie.

Aby zrozumieć, czym różni się wydajność i jaką ma wartość, najpierw szybko odświeżmy kolejność, w której kod Pythona jest oceniany.

Sekcja 6.15 Zlecenie oceny

Python ocenia wyrażenia od lewej do prawej. Zwróć uwagę, że podczas oceny zadania prawa strona jest oceniana przed lewą.

Zatem wyrażenie a = bpo prawej stronie jest oceniane jako pierwsze.

Jak poniżej pokazuje, że a[p('left')] = p('right')prawa strona jest oceniana jako pierwsza.

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

Co robi yield ?, daje, zawiesza wykonywanie funkcji i wraca do obiektu wywołującego, a następnie wznawia wykonywanie w tym samym miejscu, w którym zostało przerwane przed zawieszeniem.

Gdzie dokładnie zawieszone jest wykonanie? Mogłeś się już domyślić ... wykonanie jest zawieszone między prawą a lewą stroną wyrażenia zysku. Zatem new_val = yield old_valwykonanie jest zatrzymane na =znaku, a wartość po prawej stronie (która jest przed zawieszeniem i jest również wartością zwracaną do wywołującego) może być inna niż wartość po lewej stronie (która jest wartością przypisywaną po wznowieniu wykonanie).

yield zwraca 2 wartości, jedną po prawej, a drugą po lewej stronie.

Jak kontrolujesz wartość po lewej stronie wyrażenia zysku? za pomocą .send()metody.

6.2.9. Wyrażenia dotyczące plonów

Wartość wyrażenia yield po wznowieniu zależy od metody, która wznowiła wykonanie. Jeśli __next__()jest używany (zwykle za pomocą for lub next()wbudowanego), wynikiem jest Brak. W przeciwnym razie, jeśli send()zostanie użyta, wynikiem będzie wartość przekazana do tej metody.

user2059857
źródło
13

Te sendmetody przyrządy współprogram .

Jeśli nie spotkałeś programów Coroutines, trudno jest się nimi zająć, ponieważ zmieniają sposób przepływu programu. Możesz przeczytać dobry samouczek, aby uzyskać więcej informacji.

Jochen Ritzel
źródło
6

Słowo „plon” ma dwa znaczenia: wyprodukować coś (np. Wydać zboże) i zatrzymać się, aby pozwolić komuś / czemuś kontynuować (np. Samochody ustępujące pieszym). Obie definicje odnoszą się do yieldsłowa kluczowego Pythona ; tym, co czyni funkcje generatora wyjątkowymi, jest to, że w przeciwieństwie do zwykłych funkcji, wartości mogą być „zwracane” do wywołującego, po prostu wstrzymując, a nie kończąc funkcję generatora.

Najłatwiej jest wyobrazić sobie generator jako jeden koniec dwukierunkowej potoku z „lewym” końcem i „prawym” końcem; ta rura jest medium, przez które wartości są przesyłane między samym generatorem a ciałem funkcji generatora. Każdy koniec potoku ma dwie operacje push:, która wysyła wartość i blokuje, dopóki drugi koniec potoku nie wyciągnie wartości i nic nie zwraca; ipull, który blokuje się do momentu, gdy drugi koniec potoku wypycha wartość i zwraca przekazaną wartość. W czasie wykonywania wykonanie odbija się w tę i z powrotem między kontekstami po obu stronach potoku - każda strona działa, dopóki nie wyśle ​​wartości na drugą stronę, w którym to momencie zatrzymuje się, pozwala drugiej stronie działać i czeka na wartość w powrót, w którym to momencie druga strona zatrzymuje się i wznawia. Innymi słowy, każdy koniec potoku biegnie od momentu otrzymania wartości do momentu wysłania wartości.

Potok jest funkcjonalnie symetryczny, ale - zgodnie z konwencją, którą definiuję w tej odpowiedzi - lewy koniec jest dostępny tylko wewnątrz ciała funkcji generatora i jest dostępny za pomocą yieldsłowa kluczowego, podczas gdy prawy koniec jest generatorem i jest dostępny za pośrednictwem sendfunkcja generatora . Jako pojedyncze interfejsy do odpowiednich końców rury yieldi sendwykonują podwójne zadanie: każdy z nich wypycha i wyciąga wartości do / z ich końców rury, yieldpopychając w prawo i ciągnąc w lewo, podczas gdy sendrobi odwrotnie. Ten podwójny obowiązek jest sednem zamieszania wokół semantyki takich stwierdzeń x = yield y. Podział yieldi sendrozbicie na dwa wyraźne kroki push / pull sprawi, że ich semantyka będzie znacznie bardziej przejrzysta:

  1. Załóżmy, że gjest to generator. g.sendprzesuwa wartość w lewo przez prawy koniec potoku.
  2. Wykonywanie w kontekście gprzerw, pozwalających na uruchomienie ciała funkcji generatora.
  3. Przepychana wartość jest przesuwana w g.sendlewo yieldi odbierana na lewym końcu rury. W x = yield y, xjest przypisana do wyciągniętej wartości.
  4. Wykonywanie jest kontynuowane w treści funkcji generatora, aż do osiągnięcia następnego wiersza zawierającego yield.
  5. yieldprzesuwa wartość w prawo przez lewy koniec potoku, z powrotem do g.send. W x = yield y, yjest popychany w prawą stronę za pośrednictwem rury.
  6. Wykonywanie w ciele funkcji generatora zostaje wstrzymane, co pozwala zewnętrznemu zakresowi kontynuować od miejsca, w którym został przerwany.
  7. g.send wznawia i pobiera wartość oraz zwraca ją użytkownikowi.
  8. Kiedy g.sendzostanie ponownie wywołane, wróć do kroku 1.

Procedura ta, choć cykliczna, ma początek: kiedy g.send(None)- co next(g)jest skrótem - jest wywoływana po raz pierwszy (przekazywanie czegoś innego niż Nonedo pierwszego sendwywołania jest nielegalne ). I może mieć koniec: kiedy nie ma już yieldinstrukcji do osiągnięcia w treści funkcji generatora.

Czy widzisz, co sprawia, że yieldstwierdzenie (a dokładniej generatory) jest tak wyjątkowe? W przeciwieństwie do returnsłowa kluczowego mierzalnego , yieldjest w stanie przekazywać wartości do swojego wywołującego i odbierać wartości od swojego wywołującego bez przerywania funkcji, w której żyje! (Oczywiście, jeśli chcesz zakończyć funkcję - lub generator - dobrze jest mieć również returnsłowo kluczowe.) Kiedy yieldnapotkana jest instrukcja, funkcja generatora po prostu zatrzymuje się, a następnie wraca do miejsca, w którym została wyłączone po wysłaniu innej wartości. I sendjest tylko interfejsem do komunikacji z wnętrzem generatora z zewnątrz.

Jeśli naprawdę chcemy przełamać tę analogię push / pull / pipe tak daleko, jak to tylko możliwe, otrzymamy następujący pseudokod, który naprawdę kieruje tym, oprócz kroków 1-5, yieldi sendsą dwiema stronami tej samej rury monetowej :

  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

Kluczem jest to, że transformacja mamy rozłam x = yield yi value1 = g.send(value2)każdy do dwóch stwierdzeń: left_end.push(y)a x = left_end.pull(); i value1 = right_end.pull()i right_end.push(value2). Istnieją dwa szczególne przypadki yieldsłowa kluczowego: x = yieldi yield y. Są to odpowiednio cukier syntaktyczny dla x = yield Nonei _ = yield y # discarding value.

Aby uzyskać szczegółowe informacje dotyczące dokładnej kolejności, w jakiej wartości są przesyłane przez potok, patrz poniżej.


Poniżej znajduje się dość długi, konkretny model powyższego. Po pierwsze, należy przede wszystkim zauważyć, że dla każdego generatora g, next(g)jest dokładnie równoważne g.send(None). Mając to na uwadze, możemy skupić się tylko na tym, jak senddziała i rozmawiać tylko o ulepszaniu generatora send.

Załóżmy, że mamy

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

Teraz definicja z fgrubsza desukrów do następującej zwykłej (nie generującej) funkcji:

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()

W tej transformacji wydarzyło się co następuje f:

  1. Przenieśliśmy implementację do funkcji zagnieżdżonej.
  2. Stworzyliśmy dwukierunkowy potok, do którego left_endbędzie uzyskiwać dostęp funkcja zagnieżdżona i do którego right_endbędzie zwracany i dostępny przez zewnętrzny zasięg - right_endto jest to, co znamy jako obiekt generatora.
  3. Wewnątrz funkcji zagnieżdżonych, pierwszą rzeczą, którą zrobić, to sprawdzić, że left_end.pull()jest None, spożywania popychane wartość w procesie.
  4. W funkcji zagnieżdżonej instrukcja x = yield yzostała zastąpiona dwoma wierszami: left_end.push(y)i x = left_end.pull().
  5. Zdefiniowaliśmy sendfunkcję dla right_end, która jest odpowiednikiem dwóch wierszy, którymi zastąpiliśmy x = yield yinstrukcję w poprzednim kroku.

W tym fantastycznym świecie, w którym funkcje mogą być kontynuowane po powrocie, gjest przypisywany, right_enda następnie impl()wywoływany. Tak więc w naszym przykładzie powyżej, gdybyśmy śledzili wykonanie wiersz po wierszu, co by się stało, byłoby mniej więcej takie:

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

To odwzorowuje dokładnie 16-krokowy pseudokod powyżej.

Istnieją inne szczegóły, takie jak sposób propagowania błędów i co się dzieje, gdy osiągniesz koniec generatora (rura jest zamknięta), ale powinno to wyjaśnić, jak działa podstawowy przepływ sterowania, gdy sendjest używany.

Korzystając z tych samych zasad usuwania cukru, przyjrzyjmy się dwóm specjalnym przypadkom:

def f1(x):
    while True:
        x = yield x

def f2():  # No parameter
    while True:
        x = yield x

W większości znikają z cukru w ​​taki sam sposób, jak f, jedynymi różnicami jest sposób yieldprzekształcania instrukcji:

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

W pierwszym wartość przekazana f1jest początkowo wypychana ( zwracana ), a następnie wszystkie wartości pobierane (wysyłane) są od razu wypychane (zwracane). W drugim przypadku xnie ma (jeszcze) wartości, kiedy pojawia się po raz pierwszy push, więc UnboundLocalErrorjest podnoszony.

BallpointBen
źródło
„Argument 1 w g = f (1) został przechwycony normalnie i przypisany do y w ciele f, ale czas True jeszcze się nie rozpoczął”. Dlaczego nie? Dlaczego Python nie próbowałby uruchomić tego kodu, dopóki nie napotka np. yield?
Josh
@Josh Kursor przesuwa się do przodu dopiero po pierwszym wywołaniu send; wystarczy jedno wywołanie of, send(None)aby przesunąć kursor do pierwszej yieldinstrukcji, a dopiero potem kolejne sendwywołania faktycznie wysyłają „prawdziwą” wartość do yield.
BallpointBen
Dzięki - To ciekawe, więc interpreter wie, że funkcja f wolę yield w pewnym momencie, a zatem czekać, aż robi się sendz rozmówcą? Przy normalnym calu funkcyjnym interpreter po prostu zacząłby wykonywać fod razu, prawda? W końcu w Pythonie nie ma żadnej kompilacji AOT. Czy na pewno o to chodzi? (nie kwestionując tego, co mówisz, naprawdę jestem zdziwiony tym, co tutaj napisałeś). Gdzie mogę przeczytać więcej o tym, skąd Python wie, że musi zaczekać, zanim zacznie wykonywać resztę funkcji?
Josh
@Josh Zbudowałem ten model myślowy po prostu obserwując, jak działają różne generatory zabawek, bez żadnego zrozumienia wewnętrznych elementów Pythona. Jednak fakt, że wartość początkowa send(None)daje odpowiednią wartość (np. 1) Bez wysyłania Nonedo generatora, sugeruje, że pierwsze wywołanie sendjest przypadkiem szczególnym. Jest to trudny do zaprojektowania interfejs; jeśli pozwolisz, aby pierwszy sendwysłał dowolną wartość, to kolejność zwracanych wartości i wysyłanych wartości byłaby różna o jeden w porównaniu z obecną.
BallpointBen
Dzięki BallpointBen. Bardzo interesujące, zostawiłem tutaj pytanie , aby zobaczyć, dlaczego tak jest.
Josh
2

To też mnie zdezorientowało. Oto przykład, który podałem, próbując skonfigurować generator, który generuje i przyjmuje sygnały w naprzemiennej kolejności (wydajność, akceptacja, wydajność, akceptacja) ...

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)

Wynik to:

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!"
Piotr
źródło