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ąć?
send()
jest wywoływany do uruchomienia generatora, musi być wywołanyNone
jako 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.Odpowiedzi:
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:
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ę:Dzieje się tak, że
takesTwoSeconds()
zwraca aDeferred
, 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 dodoStuff()
funkcji. WdoStuff()
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:Jest o wiele bardziej zagmatwany i nieporęczny.
źródło
Ta funkcja służy do pisania programów
wydruki
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
Ale po wysłaniu staje się ulicą dwukierunkową
To otwiera drzwi dla użytkownika, dostosowując zachowanie generatorów w locie i generator reagujący na użytkownika.
źródło
send()
generatora nie osiągnąłyield
jeszcze słowa kluczowego .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:
Oto jak wykonałbyś ten sam typ funkcji za pomocą wysyłania, więc w każdej iteracji możesz zmienić wartość liczby:
Oto jak to wygląda, ponieważ możesz zobaczyć, że wysłanie nowej wartości liczby zmienia wynik:
Możesz również umieścić to w pętli for jako takiej:
Więcej pomocy znajdziesz w tym świetnym samouczku .
źródło
send
? Prostylambda x: x * 2
robi to samo w znacznie mniej zawiły sposób.Niektóre przypadki użycia generatora i
send()
Generatory z możliwością
send()
: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:
watched_attempt
instancję z przepisuprzy każdym sprawdzaniu wejścia, czy wejście jest oczekiwane (i kończy się niepowodzeniem, jeśli nie jest)
Aby z niego skorzystać, najpierw utwórz
watched_attempt
instancję: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:
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.
Zauważ, że:
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).
Wynik wyglądałby następująco:
źródło
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
Zatem wyrażenie
a = b
po prawej stronie jest oceniane jako pierwsze.Jak poniżej pokazuje, że
a[p('left')] = p('right')
prawa strona jest oceniana jako pierwsza.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_val
wykonanie 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
źródło
Te
send
metody 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.
źródło
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
yield
sł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ą
yield
słowa kluczowego, podczas gdy prawy koniec jest generatorem i jest dostępny za pośrednictwemsend
funkcja generatora . Jako pojedyncze interfejsy do odpowiednich końców ruryyield
isend
wykonują podwójne zadanie: każdy z nich wypycha i wyciąga wartości do / z ich końców rury,yield
popychając w prawo i ciągnąc w lewo, podczas gdysend
robi odwrotnie. Ten podwójny obowiązek jest sednem zamieszania wokół semantyki takich stwierdzeńx = yield y
. Podziałyield
isend
rozbicie na dwa wyraźne kroki push / pull sprawi, że ich semantyka będzie znacznie bardziej przejrzysta:g
jest to generator.g.send
przesuwa wartość w lewo przez prawy koniec potoku.g
przerw, pozwalających na uruchomienie ciała funkcji generatora.g.send
lewoyield
i odbierana na lewym końcu rury. Wx = yield y
,x
jest przypisana do wyciągniętej wartości.yield
.yield
przesuwa wartość w prawo przez lewy koniec potoku, z powrotem dog.send
. Wx = yield y
,y
jest popychany w prawą stronę za pośrednictwem rury.g.send
wznawia i pobiera wartość oraz zwraca ją użytkownikowi.g.send
zostanie ponownie wywołane, wróć do kroku 1.Procedura ta, choć cykliczna, ma początek: kiedy
g.send(None)
- conext(g)
jest skrótem - jest wywoływana po raz pierwszy (przekazywanie czegoś innego niżNone
do pierwszegosend
wywołania jest nielegalne ). I może mieć koniec: kiedy nie ma jużyield
instrukcji do osiągnięcia w treści funkcji generatora.Czy widzisz, co sprawia, że
yield
stwierdzenie (a dokładniej generatory) jest tak wyjątkowe? W przeciwieństwie doreturn
słowa kluczowego mierzalnego ,yield
jest 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żreturn
słowo kluczowe.) Kiedyyield
napotkana 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. Isend
jest 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,
yield
isend
są dwiema stronami tej samej rurymonetowej: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
Kluczem jest to, że transformacja mamy rozłam
x = yield y
ivalue1 = g.send(value2)
każdy do dwóch stwierdzeń:left_end.push(y)
ax = left_end.pull()
; ivalue1 = right_end.pull()
iright_end.push(value2)
. Istnieją dwa szczególne przypadkiyield
słowa kluczowego:x = yield
iyield y
. Są to odpowiednio cukier syntaktyczny dlax = yield None
i_ = 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żneg.send(None)
. Mając to na uwadze, możemy skupić się tylko na tym, jaksend
działa i rozmawiać tylko o ulepszaniu generatorasend
.Załóżmy, że mamy
Teraz definicja z
f
grubsza desukrów do następującej zwykłej (nie generującej) funkcji:W tej transformacji wydarzyło się co następuje
f
:left_end
będzie uzyskiwać dostęp funkcja zagnieżdżona i do któregoright_end
będzie zwracany i dostępny przez zewnętrzny zasięg -right_end
to jest to, co znamy jako obiekt generatora.left_end.pull()
jestNone
, spożywania popychane wartość w procesie.x = yield y
została zastąpiona dwoma wierszami:left_end.push(y)
ix = left_end.pull()
.send
funkcję dlaright_end
, która jest odpowiednikiem dwóch wierszy, którymi zastąpiliśmyx = yield y
instrukcję w poprzednim kroku.W tym fantastycznym świecie, w którym funkcje mogą być kontynuowane po powrocie,
g
jest przypisywany,right_end
a następnieimpl()
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: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
send
jest używany.Korzystając z tych samych zasad usuwania cukru, przyjrzyjmy się dwóm specjalnym przypadkom:
W większości znikają z cukru w taki sam sposób, jak
f
, jedynymi różnicami jest sposóbyield
przekształcania instrukcji:W pierwszym wartość przekazana
f1
jest początkowo wypychana ( zwracana ), a następnie wszystkie wartości pobierane (wysyłane) są od razu wypychane (zwracane). W drugim przypadkux
nie ma (jeszcze) wartości, kiedy pojawia się po raz pierwszypush
, więcUnboundLocalError
jest podnoszony.źródło
yield
?send
; wystarczy jedno wywołanie of,send(None)
aby przesunąć kursor do pierwszejyield
instrukcji, a dopiero potem kolejnesend
wywołania faktycznie wysyłają „prawdziwą” wartość doyield
.f
wolęyield
w pewnym momencie, a zatem czekać, aż robi sięsend
z rozmówcą? Przy normalnym calu funkcyjnym interpreter po prostu zacząłby wykonywaćf
od 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?send(None)
daje odpowiednią wartość (np.1
) Bez wysyłaniaNone
do generatora, sugeruje, że pierwsze wywołaniesend
jest przypadkiem szczególnym. Jest to trudny do zaprojektowania interfejs; jeśli pozwolisz, aby pierwszysend
wysłał dowolną wartość, to kolejność zwracanych wartości i wysyłanych wartości byłaby różna o jeden w porównaniu z obecną.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) ...
Wynik to:
źródło