Python: использование рекурсивного алгоритма в качестве генератора

99

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

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

def getPermutations(string, storage, prefix=""):
   if len(string) == 1:
      storage.append(prefix + string)   # <-----
   else:
      for i in range(len(string)):
         getPermutations(string[:i]+string[i+1:], storage, prefix+string[i])

storage = []
getPermutations("abcd", storage)
for permutation in storage: print permutation

(Пожалуйста, не обращайте внимания на неэффективность, это только пример.)

Теперь я хочу превратить свою функцию в генератор, то есть получить перестановку вместо добавления ее в список хранения:

def getPermutations(string, prefix=""):
   if len(string) == 1:
      yield prefix + string             # <-----
   else:
      for i in range(len(string)):
         getPermutations(string[:i]+string[i+1:], prefix+string[i])

for permutation in getPermutations("abcd"):
   print permutation

Этот код не работает (функция ведет себя как пустой генератор).

Я что-то упускаю? Есть ли способ превратить описанный выше рекурсивный алгоритм в генератор, не заменяя его итеративным ?

Федерико А. Рампони
источник

Ответы:

117
def getPermutations(string, prefix=""):
    if len(string) == 1:
        yield prefix + string
    else:
        for i in xrange(len(string)):
            for perm in getPermutations(string[:i] + string[i+1:], prefix+string[i]):
                yield perm

Или без аккумулятора:

def getPermutations(string):
    if len(string) == 1:
        yield string
    else:
        for i in xrange(len(string)):
            for perm in getPermutations(string[:i] + string[i+1:]):
                yield string[i] + perm
Маркус Жардерот
источник
29
В Python 3.4 вы можете заменить последние две строки на yield from getPermutations(string[:i] + string[i+1:]), что во многих отношениях более эффективно!
Мануэль Эберт
1
Вам все равно нужно будет каким-то образом добиться результата. Использование yield fromпотребует от вас использования аргумента аккумулятора ( prefix).
Маркус Джардерот
Предложение: Определите другой генератор, который возвращает string[i],string[:i]+string[i+1:]пары. Тогда это будет:for letter,rest in first_letter_options(string): for perm in getPermuations(rest): yield letter+perm
Thomas Andrews
29

Это позволяет избежать len(string)-глубокой рекурсии и в целом является хорошим способом работы с генераторами внутри генераторов:

from types import GeneratorType

def flatten(*stack):
    stack = list(stack)
    while stack:
        try: x = stack[0].next()
        except StopIteration:
            stack.pop(0)
            continue
        if isinstance(x, GeneratorType): stack.insert(0, x)
        else: yield x

def _getPermutations(string, prefix=""):
    if len(string) == 1: yield prefix + string
    else: yield (_getPermutations(string[:i]+string[i+1:], prefix+string[i])
            for i in range(len(string)))

def getPermutations(string): return flatten(_getPermutations(string))

for permutation in getPermutations("abcd"): print permutation

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


Python 3.3 добавит yield fromсинтаксис, который позволяет естественное делегирование суб-генератору:

def getPermutations(string, prefix=""):
    if len(string) == 1:
        yield prefix + string
    else:
        for i in range(len(string)):
            yield from getPermutations(string[:i]+string[i+1:], prefix+string[i])
эфемерный
источник
20

Внутренний вызов getPermutations - это тоже генератор.

def getPermutations(string, prefix=""):
   if len(string) == 1:
      yield prefix + string            
   else:
      for i in range(len(string)):
         getPermutations(string[:i]+string[i+1:], prefix+string[i])  # <-----

Вам нужно повторить это с помощью цикла for (см. Публикацию @MizardX, которая вытеснила меня на секунды!)

С.Лотт
источник