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:
- Załóżmy, że
gjest to generator. g.sendprzesuwa wartość w lewo przez prawy koniec potoku.
- Wykonywanie w kontekście
gprzerw, pozwalających na uruchomienie ciała funkcji generatora.
- 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.
- Wykonywanie jest kontynuowane w treści funkcji generatora, aż do osiągnięcia następnego wiersza zawierającego
yield.
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.
- Wykonywanie w ciele funkcji generatora zostaje wstrzymane, co pozwala zewnętrznemu zakresowi kontynuować od miejsca, w którym został przerwany.
g.send wznawia i pobiera wartość oraz zwraca ją użytkownikowi.
- 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 :
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 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:
- Przenieśliśmy implementację do funkcji zagnieżdżonej.
- 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.
- 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.
- W funkcji zagnieżdżonej instrukcja
x = yield yzostała zastąpiona dwoma wierszami: left_end.push(y)i x = left_end.pull().
- 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.