Сопрограммы в Python: лучшие практики
Мне интересно, каковы лучшие практики для написания сопрограмм в Python 3. Я разрабатываю базовые методы, которые должны принимать некоторые входные данные (используя метод.send()), выполнять вычисления на этих входных данных, а затем выводить выходные данные.
Первый подход, который я нашел, заключается в следующем:
def coroutine(func):
data = yield
while 1:
data = yield func(data)
Кажется, это работает, но линия в цикле изгибает мой разум. Похоже, что сначала выдают функцию, а затем принимают ввод и выполняют назначение после возобновления. Это совершенно не интуитивно для меня.
Другой подход, на который я смотрю:
def coroutine():
while 1:
data = yield
[ do stuff with data here ... ]
yield result
Этот код намного проще для меня, и он также позволяет мне поместить код прямо в генератор вместо передачи функции. Но это раздражает в использовании. После каждого фактического вызова генератора (например, "gen.send(2)") должен следовать "gen.send(None)", чтобы продвинуть генератор к следующему выходу.
Мне кажется, что проблема здесь связана с тем, что ключевое слово "yield" используется для двух разных вещей: оператор возврата и оператор ввода.
Если возможно, я хочу подход, который позволяет мне принимать входные данные, выполнять вычисления на этих входных данных, а затем выводить выходные данные без необходимости передавать функции и использовать однострочные символы, как в первом подходе, или отправлять посторонние значения, как во втором подход. Как я могу это сделать?
Обратите внимание: на самом деле я буду отправлять несколько значений. Таким образом, проблемы с посторонними утверждениями "g.send(None)" усугубляются.
3 ответа
Вы можете сделать это так же, как в первом примере. Вы просто должны "делать вещи с данными" внутри цикла. Вот пример:
def coroutine():
data = yield
while True:
print("I am doing stuff with data now")
data = data * 2
data = yield data
Вы можете использовать это так:
>>> co = coroutine()
>>> next(co)
>>> co.send(1)
I am doing stuff with data now
2
>>> co.send(88)
I am doing stuff with data now
176
Вы правы в том, что yield
играет двойную роль, и выдает результат, и принимает значение, впоследствии переданное через send
, (Подобным же образом, send
играет двойную и дополняющую роль в том, что каждый send
call возвращает значение, которое дает генератор.) Обратите внимание на порядок там: когда у вас есть yield
выражении, сначала выдается значение, а затем значение yield
выражение становится тем, что есть sent
в последствии.
Это может показаться "задом наперед", но вы можете сделать это "вперёд", выполняя это в цикле, как вы, по сути, уже сделали. Идея в том, что вы сначала дадите какое-то начальное значение (возможно, бессмысленное). Это необходимо, потому что вы не можете использовать send
до того, как значение было получено (так как не было бы yield
выражение для оценки переданного значения). Затем каждый раз, когда вы используете yield
вы выдаете "текущее" значение, одновременно принимая входные данные, которые будут использоваться при вычислении "следующего" значения.
Как я уже упоминал в комментарии, из вашего примера не ясно, почему вы вообще используете генераторы. Во многих случаях вы можете добиться аналогичного эффекта, просто написав класс, который имеет свои собственные методы для передачи и извлечения чего-либо, и если вы пишете класс, вы можете сделать API-интерфейс любым, что захотите. Если вы решите использовать генераторы, вы должны принять двойную роль ввода / вывода: send
а также yield
, Если вам это не нравится, не используйте генераторы (или, если вам нужно состояние приостановки функции, которое они предоставляют, вы можете использовать их, но оберните их классом, который отделяет отправку от уступки).
защита myGenerator (список):
Цитата
print('start')
s = None
for item in list:
for j in range(1, len(list)):
user = yield s
if user == 'plus':
item += list[j]
s = item
func = мояФункция([1, 2, 3, 5])
следующий (функция)
печать(func.send('плюс'))
печать(func.send('плюс'))
печать(func.send('плюс'))
To add an important clarification to BrenBarn's answer: the sentence "when you have a yield expression, it first yields the value out, and then the value of the yield expression becomes whatever is sent in afterwards." isn't completely accurate and only happens in the example he gave because the same yield is used in a loop. What actually occurs is the yield assignment is made first (at the yield where the program had paused) and then execution continues to the next yield which returns its result.
When you use the send() method, it will make the assignment at the yield where execution was paused (but not return a result from THAT yield) and then continue up to the next yield at which point a value will be returned and execution will pause. This is demonstrated in the following graphic and example code:
Этот код с использованием Python 3.8 демонстрирует / подтверждает операцию, описанную выше:
def GenFunc():
x = 'a'
in1 = yield x
y = 'b'
print(f"After first yield: {in1=}, {y=}")
in2 = yield y
z = 'c'
print(f"After second yield: {in1=}, {in2=}")
in3 = yield z
print(f"After third yield: {in1=}, {in2=}, {in3=}")
Что выполняется следующим образом:
>>> mygen = GenFunc()
>>> next(mygen)
Out: 'a'
>>> mygen.send(25)
After first yield: in1=25, y='b'
Out: 'b'
>>> mygen.send(15)
After second yield: in1=25, in2=15
Out: 'c'
>>> mygen.send(45)
After third yield: in1=25, in2=15, in3=45
-----------------------------
StopInteration Error
А вот дополнительный пример, демонстрирующий то же поведение с одним yield в цикле:
def GenFunc(n):
x = 0
while True:
n += 1
x = yield n,x
x += 1
print(n,x)
x += 1
Что выполняется следующим образом:
>>> mygen = GenFunc(10)
>>> next(mygen)
Out: (11, 0)
>>> mygen.send(5)
11 6
Out: (12, 7)