Слово «урожайность» имеет два значения: производить что-то (например, приносить кукурузу) и останавливать, чтобы позволить кому-то / чему-либо еще продолжать движение (например, автомобили, уступающие пешеходам). Оба определения применяются к yieldключевому слову Python ; что делает функции генератора особенными, так это то, что в отличие от обычных функций, значения могут быть «возвращены» вызывающей стороне, просто останавливая, а не завершая функцию генератора.
Проще всего представить генератор как один конец двунаправленной трубы с «левым» концом и «правым» концом; этот канал является средой, по которой значения передаются между самим генератором и телом функции генератора. Каждый конец канала имеет две операции: pushотправляет значение и блокирует, пока другой конец канала не извлекает значение и ничего не возвращает; иpull, который блокируется до тех пор, пока другой конец канала не вытолкнет значение и не вернет переданное значение. Во время выполнения выполнение отскакивает назад и вперед между контекстами по обе стороны канала - каждая сторона выполняется до тех пор, пока не отправит значение другой стороне, после чего остановится, разрешит выполнение другой стороне и ожидает значения в Возврат, после чего другая сторона останавливается, и она возобновляется. Другими словами, каждый конец канала работает с момента получения значения до момента отправки значения.
Канал является функционально-симметричным, но - по соглашению, который я определил в этом ответе - левый конец доступен только внутри тела функции генератора и доступен через yieldключевое слово, в то время как правый конец является генератором и доступен через sendфункция генератора . Как особые интерфейсы к их соответствующим концам трубы, yieldи sendвыполняют двойную обязанность: каждый из них и толкает и тянет значения к / от их концов трубы, yieldтолкая вправо и тянет влево, в то время как sendделает противоположное. Эта двойная обязанность - суть путаницы вокруг семантики таких утверждений, как x = yield y. Ломать yieldи sendвниз в два этапа явно тяни / толкай сделает их семантика гораздо более ясно:
- Предположим,
gэто генератор. g.sendтолкает значение влево через правый конец трубы.
- Выполнение в контексте
gпауз, позволяющих запустить тело функции генератора.
- Выдвигаемое значение
g.sendтянется влево yieldи принимается на левом конце трубы. В x = yield y, xприсваивается значение вытягивания.
- Выполнение продолжается в теле функции генератора, пока не
yieldбудет достигнута следующая строка, содержащая .
yieldтолкает значение вправо через левый конец трубы, обратно до g.send. В x = yield y, yпроталкивается вправо через трубу.
- Выполнение в теле функции генератора приостанавливается, позволяя внешней области продолжить там, где она остановилась.
g.send возобновляет и извлекает значение и возвращает его пользователю.
- При
g.sendследующем вызове вернитесь к шагу 1.
Несмотря на цикличность, эта процедура имеет начало: когда g.send(None)- то, что next(g)является сокращением - сначала вызывается (нельзя передавать что-либо, кроме Noneпервого sendвызова). И это может иметь конец: когда больше нет yieldоператоров, которые должны быть достигнуты в теле функции генератора.
Вы понимаете, что делает yieldзаявление (или, точнее, генераторы) таким особенным? В отличие от returnключевого слова « трусливый» , yieldон может передавать значения своему вызывающему и получать значения от своего вызывающего без прерывания функции, в которой он живет! (Конечно, если вы хотите завершить функцию - или генератор - также удобно иметь returnключевое слово.) Когда встречается yieldоператор, функция генератора просто делает паузу, а затем возвращается обратно туда, где она оставлена выкл при отправке другого значения. И sendэто просто интерфейс для связи с внутренней функцией генератора извне.
Если мы действительно хотим сломать эту аналогию «push / pull / pipe» настолько далеко, насколько сможем, мы получим следующий псевдокод, который действительно возвращает домой, помимо шагов 1-5, yieldи sendявляется двумя сторонами одной и той же трубы монет :
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
Ключ преобразования является то , что мы имеем раскол x = yield yи value1 = g.send(value2)каждую из двух утверждений: left_end.push(y)и x = left_end.pull(); и value1 = right_end.pull()и right_end.push(value2). Есть два особых случая yieldключевого слова: x = yieldи yield y. Это синтаксический сахар соответственно для x = yield Noneи _ = yield y # discarding value.
Конкретные детали относительно точного порядка, в котором значения отправляются через канал, см. Ниже.
Далее следует довольно длинная конкретная модель вышесказанного. Во- первых, следует в первую очередь отметить , что для любого генератора g, next(g)точно эквивалентно g.send(None). Имея это в виду, мы можем сосредоточиться только на том, как sendработает, и говорить только о продвижении генератора с send.
Предположим, у нас есть
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
Теперь определение fгрубо десугаров для следующей обычной (не генераторной) функции:
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()
В этой трансформации произошло следующее f:
- Мы переместили реализацию во вложенную функцию.
- Мы создали двунаправленный канал,
left_endк которому будет обращаться вложенная функция и right_endкоторый будет возвращен и получен доступ из внешней области видимости - right_endэто то, что мы знаем как объект генератора.
- В рамках вложенной функции самое первое, что мы делаем, это проверяем, то
left_end.pull()есть Noneпотребляемое значение в процессе.
- Внутри вложенной функции оператор
x = yield yзаменен двумя строками: left_end.push(y)и x = left_end.pull().
- Мы определили
sendфункцию для right_end, которая является аналогом двух строк, которыми мы заменили x = yield yоператор на предыдущем шаге.
В этом фэнтезийном мире, где функции могут продолжаться после возвращения, gназначается right_endи затем impl()вызывается. Итак, в нашем примере выше, если бы мы следили за выполнением строки за строкой, то примерно следующее:
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
Это отображается точно в 16-шаговый псевдокод выше.
Существуют и другие детали, например, как распространяются ошибки и что происходит, когда вы достигаете конца генератора (канал закрыт), но это должно прояснить, как работает основной поток управления при sendиспользовании.
Используя те же правила десагеринга, давайте рассмотрим два особых случая:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
По большей части они desugar так же, как f, единственные различия в yieldтом, как трансформируются утверждения:
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
Во-первых, переданное значение f1сначала отбрасывается (уступается), а затем все значения, которые извлекаются (отправляются), возвращаются (возвращаются) назад. Во втором, xне имеет значения (пока), когда это впервые приходит push, так что возникает UnboundLocalError.