Концептуально мы все знаем о многопроцессорности и многопоточности, а сопрограммы на самом деле многопараллельны в одном потоке. Синтаксически сопрограммы похожи на генераторы в том смысле, что они являются функциями, тело определения которых содержит ключевое слово yield. Разница в том, что доходность сопрограммы обычно появляется в правой части выражения:datum = yield. Это заставляет новичков чувствовать, что ключевое слово yield на мгновение не пахнет Я думал, что yield просто приостанавливает выполнение и возвращает значение, а результат можно разместить справа?
От генераторов к сопрограммам
Давайте сначала рассмотрим возможный пример простейшего использования сопрограмм:
>>> def simple_coroutine():
... print("-> coroutine started")
... x = yield
... print("-> coroutine received:", x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x0000019A681F27B0>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
File "<input>", line 1, in <module>
StopIteration
Причина, по которой yield может быть помещена справа, заключается в том, что сопрограмма может получать значение, переданное вызывающей стороной с помощью .send().
После того, как yield помещен справа, выражение может быть помещено справа, см. следующий пример:
def simple_coro2(a):
b = yield a
c = yield a + b
my_coro2 = simple_coro2(14)
next(my_coro2)
my_coro2.send(28)
my_coro2.send(99)
Процесс исполнения таков:
Вызовите next(my_coro2), выполните yield a и выведите 14.
Вызовите my_coro2.send(28), назначьте 28 для b, а затем выполните yield a + b для вывода 42.
Вызовите my_coro2.send(99), присвойте 99 переменной c, и сопрограмма завершится.
Отсюда следует, что для строки b = yield a код справа от = выполняется до присваивания.
В примере вам нужно вызвать next(my_coro), чтобы запустить генератор, дать программе сделать паузу в операторе yield, а затем отправить данные. Это потому, что сопрограммы имеют четыре состояния:
GEN_CREATED ожидает начала выполнения
Выполняется интерпретатор GEN_RUNNING
«GEN_SUSPENDED» приостанавливается в выражении yield
Выполнение GEN_CLOSED завершается
Данные могут быть отправлены только в состоянии GEN_SUSPENDED. Этот шаг, выполненный заранее, называется предварительным возбуждением. Вы можете либо вызвать next(my_coro)pre-возбуждение, либо my_coro.send(None) предварительное возбуждение, эффект тот же.
предварительно возбужденная сопрограмма
Сопрограмма должна быть предварительно возбуждена, прежде чем ее можно будет использовать, то есть перед отправкой вызовите next, чтобы перевести сопрограмму в состояние GEN_SUSPENDED. Но об этом часто забывают. Чтобы не забыть, определите декоратор prefire, например:
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
Но на самом деле Python предлагает более элегантный способ, называемый yield from, который автоматически предварительно возбуждает сопрограммы.
Пользовательские декораторы prefire и yield from несовместимы.
yield from
yield from эквивалентно ключевому слову await в других языках Функция такова: при использовании yield from subgen() в генераторе gen, subgen получит управление и передаст выходное значение вызывающей стороне gen, то есть вызывающая сторона может контролировать подген напрямую. Между тем, gen блокируется, ожидая завершения работы subgen.
yield from можно использовать для упрощения yield in for циклов:
for c in "AB":
yield c
yield from "AB"
Первое, что делает выражение yield from x с x, — это вызывает iter(x), чтобы получить из него итератор.
Но роль доходности гораздо больше, ее более важная роль — открыть двусторонний канал. Как показано ниже:
Эта схема очень информативна и сложна для понимания.
В первую очередь необходимо понять эти три понятия: вызывающая сторона, делегирующий генератор, подгенератор.
абонент
Грубо говоря, это основная функция, которая является известной основной функцией входа в программу.
# the client code, a.k.a. the caller def main(data): # <8> results = {} for key, values in data.items(): group = grouper(results, key) # <9> next(group) # <10> for value in values: group.send(value) # <11> group.send(None) # important! <12> # print(results) # uncomment to debug report(results)
Построитель делегатов
Это функция, содержащая оператор yield from, то есть сопрограмма.
# the delegating generator def grouper(results, key): # <5> while True: # <6> results[key] = yield from averager() # <7>
вспомогательный генератор
Это подпрограмма, за которой следует правая часть оператора yield from.
# the subgenerator def averager(): # <1> total = 0.0 count = 0 average = None while True: term = yield # <2> if term is None: # <3> break total += term count += 1 average = total/count return Result(count, average) # <4>
Это намного удобнее, чем выглядит термин.
Затем 5 строк: send, yield, throw, StopIteration, close.
send
Когда сопрограмма приостанавливается в выражении yield from, основная функция может отправлять данные в подпрограмму после правой части оператора yield from через выражение yield from.
yield
Подпрограмма, следующая за правой частью оператора yield from, отправляет выходное значение в основную функцию через выражение yield from.
throw
Основная функция передает значение None через group.send(None), так что цикл while подпрограммы, следующий за правой частью оператора yield from, завершается, так что управление возвращается к сопрограмму для продолжения выполнения, в противном случае она будет временно приостановлена в операторе yield from.
StopIteration
После того, как функция-генератор справа от оператора yield from возвращает значение, интерпретатор выдает исключение StopIteration. И добавьте возвращаемое значение к объекту исключения, после чего сопрограмма возобновится.
close
После выполнения основной функции вызывается метод close() для выхода из сопрограммы.
Общий процесс понятен, а более технические детали изучать дальше не буду, если у вас есть время, вы можете изучить его в будущей серии принципов Python.
yield from часто используется вместе с декоратором @asyncio.coroutine в стандартной библиотеке Python 3.4.
Корутины, используемые в качестве аккумуляторов
Это обычное использование сопрограмм, код выглядит следующим образом:
def averager():
total = 0.0
count = 0
average = None
while True: # <1>
term = yield average # <2>
total += term
count += 1
average = total/count
Корутины реализуют параллелизм
Пример здесь немного сложный
Основной фрагмент кода:
# BEGIN TAXI_PROCESS
def taxi_process(ident, trips, start_time=0): # <1>
"""Yield to simulator issuing event at each state change"""
time = yield Event(start_time, ident, 'leave garage') # <2>
for i in range(trips): # <3>
time = yield Event(time, ident, 'pick up passenger') # <4>
time = yield Event(time, ident, 'drop off passenger') # <5>
yield Event(time, ident, 'going home') # <6>
# end of taxi process # <7>
# END TAXI_PROCESS
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
seed=None):
"""Initialize random generator, build procs and run simulation"""
if seed is not None:
random.seed(seed) # get reproducible results
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
for i in range(num_taxis)}
sim = Simulator(taxis)
sim.run(end_time)
В этом примере показано, как обрабатывать события в основном цикле и как управлять сопрограммами путем отправки данных. Это основная идея пакета asyncio. Используйте сопрограммы вместо потоков и обратных вызовов для достижения параллелизма.