Слово "врожайність" має два значення: виробляти щось (наприклад, давати урожай кукурудзи) і зупиняти, щоб хтось / щось інше продовжував (наприклад, машини, що піддаються пішоходам). Обидва визначення стосуються Python'syield ключового слова ; Що робить функції генератора особливими, це те, що на відміну від звичайних функцій, значення можуть бути "повернені" абоненту, просто призупинивши, не припиняючи, функцію генератора.
Найпростіше уявити генератор як один кінець двонаправленої труби з "лівим" кінцем і "правим" кінцем; ця труба є середовищем, над яким надсилаються значення між самим генератором і тілом функції генератора. Кожен кінець труби має дві операції: pushяка надсилає значення і блокує, поки інший кінець труби не витягне значення і нічого не поверне; іpull, який блокує, поки інший кінець труби не натисне значення, і повертає висунуте значення. Під час виконання виконання виконує відхилення вперед і назад між контекстами з обох боків труби - кожна сторона працює, поки не надішле значення іншій стороні, після чого вона зупиняється, дозволяє іншій стороні бігати і чекає значення в повернення, після чого інша сторона зупиняється і вона поновлюється. Іншими словами, кожен кінець труби проходить з моменту отримання значення до моменту, коли він надсилає значення.
Труба функціонально симетрична, але - за умовою, яку я визначаю у цій відповіді - лівий кінець доступний лише у тілі функції генератора і доступний за допомогою yieldключового слова, тоді як правий кінець - генератор і доступний через sendфункція генератора . Як єдині інтерфейси до відповідних кінців труби, yieldі sendвиконують подвійний обов'язок: вони обидва штовхають і тягнуть значення до / з кінців труби, yieldштовхаючи вправо і тягнучи вліво, роблячи sendнавпаки. Цей подвійний обов'язок є сутністю плутанини навколо семантики висловлювань x = yield y. Розбивання yieldта sendзменшення двох чітких кроків "push / pull" зробить їх семантику набагато зрозумілішою:
- Припустимо,
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
Здебільшого вони дегуртують так само, як 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піднімається.