Генераторы и сопрограммы Python - PullRequest
9 голосов
/ 10 мая 2011

Я изучаю сопрограммы и генераторы на разных языках программирования.

Мне было интересно, есть ли более чистый способ объединить две сопрограммы, реализованные с помощью генераторов, чем возвращаться к вызывающей стороне независимо от того, что выдает вызываемый объект?

Допустим, мы используем следующее соглашение: все выходы, кроме последнего, возвращают ноль, а последний возвращает результат сопрограммы. Так, например, у нас может быть сопрограмма, которая вызывает другую:

def A():
  # yield until a certain condition is met
  yield result

def B():
  # do something that may or may not yield
  x = bind(A())
  # ...
  return result

в этом случае мне бы хотелось, чтобы посредством bind (который может или не может быть реализован, вот в чем вопрос) сопрограмма B дает всякий раз, когда A уступает, пока A не возвращает свой конечный результат, который затем присваивается x, позволяя B продолжать.

Я подозреваю, что фактический код должен явно повторять A так:

def B():
  # do something that may or may not yield
  for x in A(): ()
  # ...
  return result

, что немного уродливо и подвержено ошибкам ...

PS: это для игры, где пользователями языка будут дизайнеры, которые пишут скрипты (script = сопрограмма). У каждого персонажа есть связанный скрипт, и есть много подпрограмм, которые вызываются основным скриптом; Учтите, что, например, run_ship многократно вызывает reach_closest_enemy, fight_with_closest_enemy, flee_to_allies и так далее. Все эти подпрограммы должны вызываться так, как вы описали выше; для разработчика это не проблема, но для дизайнера, чем меньше кода им нужно написать, тем лучше!

Ответы [ 2 ]

17 голосов
/ 11 ноября 2011

Редактировать: я рекомендую использовать Гринлет .Но если вы заинтересованы в чистом подходе Python, читайте дальше.

Этот вопрос рассматривается в PEP 342 , но сначала это довольно сложно понять.Я постараюсь просто объяснить, как это работает.

Для начала позвольте мне подвести итог тому, что я считаю проблемой, которую вы действительно пытаетесь решить.

Проблема

У вас есть стек вызовов функций генератора, вызывающих другие функции генератора.То, что вы действительно хотите - это иметь возможность получать доход от генератора наверху, и иметь выход, распространяющийся по всему стеку.

Проблема в том, что Python не ( на уровне языка) поддерживает реальные сопрограммы, только генераторы.(Но они могут быть реализованы.) Реальные сопрограммы позволяют вам останавливать весь стек вызовов функций и переключаться на другой стек.Генераторы позволяют вам остановить только одну функцию.Если генератор f () хочет выдать, оператор yield должен быть в f (), а не в другой функции, которую вызывает f ().

Решение, которое, я думаю, вы используете сейчас, состоит в том, чтобысделайте что-то подобное в ответе Саймона Стеллинга (то есть вызовите f () для вызова g (), получив все результаты g ()).Это очень многословно и безобразно, и вы ищете синтаксический сахар, чтобы обернуть этот шаблон.Обратите внимание, что это по существу разматывает стек каждый раз, когда вы сдаете, а затем снова заводите его обратно.

Решение

Существует лучший способ решения этой проблемы.Вы в основном реализуете сопрограммы, запуская свои генераторы поверх системы «батут».

Чтобы это работало, вам нужно следовать паре паттернов: 1. Когда вы хотите вызвать другую сопрограмму, выведите ее.2. Вместо того, чтобы возвращать значение, выведите его.

, поэтому

def f():
    result = g()
    # …
    return return_value

становится

def f():
    result = yield g()
    # …
    yield return_value

Скажем, вы в f ().Батутная система называется F ().Когда вы даете генератор (скажем, g ()), батутная система вызывает g () от вашего имени.Затем, когда g () закончила давать значения, батутная система перезапускает f ().Это означает, что вы на самом деле не используете стек Python;вместо этого система батута управляет стеком вызовов.

Когда вы получаете что-то, кроме генератора, система батута обрабатывает это как возвращаемое значение.Он передает это значение обратно генератору вызывающей стороны через оператор yield (используя метод генераторов .send ()).

Комментарии

Этот тип системы чрезвычайно важен и полезен в асинхронных приложениях,как те, которые используют Торнадо или Twisted.Вы можете остановить весь стек вызовов, когда он заблокирован, пойти и сделать что-то еще, а затем вернуться и продолжить выполнение первого вызова, где он остановился.

Недостаток вышеприведенного решения заключается в том, что оно требует от вас записипо сути все ваши функции как генераторы.Возможно, лучше использовать реализацию истинных сопрограмм для Python - см. Ниже.

Альтернативы

Существует несколько реализаций сопрограмм для Python, см .: http://en.wikipedia.org/wiki/Coroutine#Implementations_for_Python

Гринлет - отличный выбор.Это модуль Python, который изменяет интерпретатор CPython, чтобы разрешить истинные сопрограммы путем замены стека вызовов.

Python 3.3 должен предоставлять синтаксис для делегирования подгруппе, см. PEP 380 .

2 голосов
/ 10 мая 2011

Вы ищете что-то подобное?

def B():
   for x in A():
     if x is None:
       yield
     else:
       break

   # continue, x contains value A yielded
...