Структурированное программирование и генераторы Python? - PullRequest
6 голосов
/ 17 ноября 2009

Обновление: Все, что я действительно хотел, было зелеными .


Примечание: Этот вопрос немного мутировал, когда люди отвечали и заставляли меня «повышать ставки», поскольку мои тривиальные примеры имели тривиальные упрощения; вместо того, чтобы продолжать мутировать это здесь, я отвечу на вопрос, когда у меня это будет яснее в голове, согласно предложению Алекса.


Генераторы Python - вещь прекрасная, но как я могу легко разбить один на модули (структурированное программирование)? Я действительно хочу PEP 380 , или хотя бы что-то сопоставимое по синтаксической нагрузке, но в существующем Python (например, 2.6)

В качестве (по общему признанию, глупого) примера возьмем следующее:

def sillyGenerator():
  for i in xrange(10):
    yield i*i
  for i in xrange(12):
    yield i*i
  for i in xrange(8):
    yield i*i

Будучи горячим сторонником СУХОЙ, я замечаю здесь повторяющуюся модель и вычленяю ее в метод:

def quadraticRange(n):
  for i in xrange(n)
    yield i*i

def sillyGenerator():
  quadraticRange(10)
  quadraticRange(12)
  quadraticRange(8)

... что, конечно, не работает. Родитель должен вызвать новую функцию в цикле, чтобы получить результаты:

def sillyGenerator():
  for i in quadraticRange(10):
    yield i
  for i in quadraticRange(12):
    yield i
  for i in quadraticRange(8):
    yield i

... что даже дольше, чем раньше!

Если я хочу вставить часть генератора в функцию, мне всегда нужна эта довольно многословная оболочка из двух строк для ее вызова. Становится хуже, если я хочу поддержать send ():

def sillyGeneratorRevisited():
  g = subgenerator()
  v = None
  try:
    while True:
      v = yield g.send(v)
  catch StopIteration:
    pass
  if v < 4:
    # ...
  else:
    # ...

И это без учета передачи исключений. Один и тот же шаблон каждый раз! Тем не менее, нельзя применять DRY и включать этот идентичный код в функцию, потому что ... вам нужен шаблон для его вызова! Я хочу что-то вроде:

def sillyGenerator():
  yield from quadraticRange(10)
  yield from quadraticRange(12)
  yield from quadraticRange(8)

def sillyGeneratorRevisited():
  v = yield from subgenerator()
  if v < 4:
    # ...
  else:
    # ...

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

Ответы [ 8 ]

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

Тем не менее, я хотел бы сделать мой Критерий повторного использования на одну ступень сложнее: Что делать, если мне нужна структура управления вокруг моего повторного поколения?

itertools часто помогает даже там - вам нужно привести конкретные примеры, где вы думаете, что это не так.

Для Например, я мог бы позвонить Субгенератор навсегда с разными параметры.

itertools.chain.from_iterable.

Или мои субгенераторы могут быть очень дорогостоящим, и я только хочу начать их, как и когда они достиг.

И chain, и chain_from_iterable делают это - никакой итератор не запускается до тех пор, пока не понадобится первый элемент из него.

Или (и это настоящий желание) Я мог бы хотеть изменить то, что я делаю затем на основе того, что мой контроллер передает мне, используя send ().

Конкретный пример был бы очень признателен. В любом случае, в худшем случае вы будете кодировать for x in blargh: yield x, где приостановленный Pep3080 позволит вам кодировать yield from blargh - около 4 дополнительных символов (не трагедия; -).

И если какая-либо изощренная сопрограммная версия некоторых функций itertools (itertools в основном поддерживает итераторы - пока нет эквивалентного модуля coroutools) становится оправданной, поскольку в вашем коде часто повторяется определенный образец композиции сопрограмм, то это не так уж сложно закодируй это сам.

Например, предположим, что мы часто делаем что-то вроде: сначала получаем определенное значение; затем, многократно, если мы отправляем 'foo', получаем следующий элемент из fooiter, если 'bla', из blaiter, если 'zop', из zopiter, что-нибудь еще, из defiter. Как только мы обнаружим второе вхождение этого композиционного шаблона, мы можем закодировать:

def corou_chaiters(initsend, defiter, val2itermap):
  currentiter = iter([initsend])
  while True:
    val = yield next(currentiter)
    currentiter = val2itermap(val, defiter)

и вызывайте эту простую композиционную функцию по мере необходимости. Если нам нужно будет составить другие сопрограммы, а не общие итераторы, у нас будет немного другой композитор, использующий метод send вместо следующей встроенной функции; и пр.

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

6 голосов
/ 17 ноября 2009

Непрактичный (к сожалению) ответ:

from __future__ import PEP0380

def sillyGenerator():
    yield from quadraticRange(10)
    yield from quadraticRange(12)
    yield from quadraticRange(8)

Потенциально практическая справка: Синтаксис для делегирования подгруппе

К сожалению, сделать это непрактичным: Мораторий языка Python

ОБНОВЛЕНИЕ Фев 2011:

Мораторий был отменен, и PEP 380 включен в список TODO для Python 3.3. Надеюсь, этот ответ скоро будет практичным.

Читать Замечания Гвидо о comp.python.devel

6 голосов
/ 17 ноября 2009

Вы хотите объединить несколько итераторов вместе:

from itertools import chain

def sillyGenerator(a,b,c):
    return chain(quadraticRange(a),quadraticRange(b),quadraticRange(c))
3 голосов
/ 17 ноября 2009

Существует предложение по улучшению Python для предоставления оператора yield from для "делегирования генерации". Ваш пример будет записан как:

def sillyGenerator():
  sq = lambda i: i * i
  yield from map(sq, xrange(10))
  yield from map(sq, xrange(12))
  yield from map(sq, xrange(8))

Или лучше, в духе СУХОГО:

def sillyGenerator():
  for i in [10, 12, 8]:
    yield from quadraticRange(i)

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

3 голосов
/ 17 ноября 2009
import itertools

def quadraticRange(n):
  for i in xrange(n)
    yield i*i

def sillyGenerator():
  return itertools.chain(
    quadraticRange(10),
    quadraticRange(12),
    quadraticRange(8),
  )

def sillyGenerator2():
  return itertools.chain.from_iterable(
    quadraticRange(n) for n in [10, 12, 8])

Последний полезен, если вы хотите убедиться, что один итератор исчерпан до запуска другого (включая его код инициализации).

2 голосов
/ 17 ноября 2009

Для произвольного количества звонков на quadraticRange:

from itertools import chain

def sillyGenerator(*args):
    return chain(*map(quadraticRange, args))

В этом коде используются map и itertools.chain. Он принимает произвольное количество аргументов и передает их в порядке quadraticRange. Получающиеся итераторы затем объединяются в цепочки.

1 голос
/ 31 декабря 2009

Существует шаблон, который я называю «ядром генератора», в котором генераторы уступают не непосредственно пользователю, а некоторому циклу «ядра», который (некоторые из них) рассматривает свои выходы как «системные вызовы» со специальным значением. *

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

import functools, types

def flatten(values_or_generators):
    for x in values_or_generators:
        if isinstance(x, GeneratorType):
            for y in x:
                yield y
        else:
            yield x

# Better name anyone?
def subgenerator(g):
    """Decorator making ``yield <gen>`` mean ``yield from <gen>``."""

    @functools.wraps(g)
    def flat_g(*args, **kw):
        return flatten(g(*args, **kw))
    return flat_g

и тогда вы можете просто написать:

def quadraticRange(n):
    for i in xrange(n)
        yield i*i

@subgenerator
def sillyGenerator():
    yield quadraticRange(10)
    yield quadraticRange(12)
    yield quadraticRange(8)

Обратите внимание, что subgenerator () разворачивает ровно один уровень иерархии. Вы можете легко сделать его многоуровневым (управляя ручным стеком или просто заменив внутренний цикл на for y in flatten(x): - но я думаю, что это лучше, чем есть, так что каждый генератор, который хочет использовать этот нестандартный синтаксис, должен быть явно заключенным в @ subgenerator.

Обратите внимание, что обнаружение генераторов несовершенно! Он обнаружит вещи, написанные как генераторы, но это деталь реализации. Все, что вас волнует как вызывающего генератора, это то, что он возвращает итератор. Это может быть функция, возвращающая некоторый объект itertools, и тогда этот декоратор завершится ошибкой.

Проверка, имеет ли объект метод .next(), слишком широка - вы не сможете получить строки без их разборки. Таким образом, наиболее надежным способом было бы проверить наличие какого-либо явного маркера, чтобы вы написали, например ,::1015*

@subgenerator
def sillyGenerator():
    yield 'from', quadraticRange(10)
    yield 'from', quadraticRange(12)
    yield 'from', quadraticRange(8)

Эй, это почти как ПКП!

[кредиты: этот ответ дает аналогичную функцию - но он глубокий (что я считаю неправильным) и не оформлен как декоратор]

0 голосов
/ 03 января 2011
class Communicator:
    def __init__(self, inflow):
        self.outflow = None
        self.inflow = inflow

Тогда вы делаете:

c = Communicator(something)
yield c
response = c.outflow

И вместо стандартного кода вы можете просто сделать:

 for i in run():
     something = i.inflow
     # ...
     i.outflow = value_to_return_back

Это достаточно простой код, который работает без особых усилий.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...