Чи можуть генератори бути рекурсивними?


79

Я наївно намагався створити рекурсивний генератор. Не працював. Це те, що я зробив:

def recursive_generator(lis):
    yield lis[0]
    recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(k)

Я отримав лише перший предмет 6.

Чи є спосіб змусити такий код працювати? По суті перенесення yieldкоманди на рівень вище за схемою рекурсії?


10
Ви не поступаєтесь, коли ви знову телефонуєте. Він потрапляє на першу дохідність, не бачить іншого звіту про прибутковість і виходить.
Morgan Thrapp

2
вам або потрібно yield from another_generator(), або вказувати кожен елемент по одному явно в циклі while. Чи another_generator()є "рекурсив" у вашій термінології - це не має значення.
Лукаш Рогальський

Відповіді:


119

Спробуйте це:

def recursive_generator(lis):
    yield lis[0]
    yield from recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(k)

Я повинен зазначити, що це не працює через помилку у вашій функції. Ймовірно, він повинен включати чек, який lisне є порожнім, як показано нижче:

def recursive_generator(lis):
    if lis:
        yield lis[0]
        yield from recursive_generator(lis[1:])

Якщо ви користуєтесь Python 2.7 і не маєте yield from, перевірте це питання.


26

Чому ваш код не виконував цю роботу

У вашому коді функція генератора:

  1. повертає (дає) перше значення списку
  2. тоді він створює новий об'єкт-ітератор, що викликає ту саму функцію генератора, передаючи йому фрагмент списку
  3. а потім зупиняється

Другий екземпляр ітератора, той, що рекурсивно створений , ніколи не повторюється. Ось чому ви отримали лише перший пункт списку.

Функція генератора корисна для автоматичного створення об'єкта ітератора (об'єкта, що реалізує протокол ітератора ), але тоді вам потрібно виконати ітерацію над ним: або вручну викликати next()метод до об'єкта, або за допомогою оператора циклу, який автоматично використовуватиме протокол ітератора.

Отже, чи можемо ми рекурсивно викликати генератор?

Відповідь - так . Тепер повернімось до свого коду, якщо ви дійсно хочете зробити це за допомогою функції генератора, я думаю, ви можете спробувати:

def recursive_generator(some_list):
    """
    Return some_list items, one at a time, recursively iterating over a slice of it... 
    """
    if len(some_list)>1:
    # some_list has more than one item, so iterate over it
        for i in recursive_generator(some_list[1:]):
            # recursively call this generator function to iterate over a slice of some_list.
            # return one item from the list.
            yield i
        else:
            # the iterator returned StopIteration, so the for loop is done.
            # to finish, return the only value not included in the slice we just iterated on.
            yield some_list[0]
    else:
        # some_list has only one item, no need to iterate on it.
        # just return the item.
        yield some_list[0]

some_list = [6,3,9,1]
for k in recursive_generator(some_list):
    print(k)

Примітка: товари повертаються в зворотному порядку, тому, можливо, ви захочете їх використовуватиsome_list.reverse() перед першим викликом генератора.

Важливо відзначити в цьому прикладі: функція генератора рекурсивно викликає себе в цикл for , який бачить ітератор і автоматично використовує на ньому протокол ітерації, тому фактично отримує з нього значення.

Це працює, але я думаю, що це насправді не корисно . Ми використовуємо функцію генератора, щоб перебирати список і просто витягувати елементи по одному, але ... список сам по собі є ітерабельним, тому генераторів немає потреби! Звичайно, я розумію, це лише приклад, можливо, є корисні програми цієї ідеї.

Інший приклад

Давайте переробимо попередній приклад (для лінощів). Скажімо, нам потрібно надрукувати елементи у списку, додаючи до кожного елемента кількість попередніх елементів (лише випадковий приклад, не обов’язково корисний).

Код буде таким:

def recursive_generator(some_list):
    """
    Return some_list items, one at a time, recursively iterating over a slice of it...
    and adding to every item the count of previous items in the list
    """
    if len(some_list)>1:
    # some_list has more than one item, so iterate over it
        for i in recursive_generator(some_list[1:]):
            # recursively call this generator function to iterate over a slice of some_list.
            # return one item from the list, but add 1 first. 
            # Every recursive iteration will add 1, so we basically add the count of iterations.
            yield i + 1
        else:
            # the iterator returned StopIteration, so the for loop is done.
            # to finish, return the only value not included in the slice we just iterated on.
            yield some_list[0]
    else:
        # some_list has only one item, no need to iterate on it.
        # just return the item.
        yield some_list[0]

some_list = [6,3,9,1]
for k in recursive_generator(some_list):
    print(k)

Тепер, як ви можете бачити, функція генератора насправді щось робить перед поверненням елементів списку І використання рекурсії починає мати сенс. Все-таки просто дурний приклад, але ви зрозуміли ідею.

Примітка: звичайно, у цьому дурному прикладі список, як очікується, міститиме лише цифри. Якщо ви дійсно хочете спробувати розірвати це, просто вкажіть рядок у some_list і отримуйте задоволення. Знову ж таки, це лише приклад, а не виробничий код!


Дуже дякую.
Цілий

14

Рекурсивні генератори корисні для обходу нелінійних структур. Наприклад, нехай двійкове дерево буде або None, або кортеж значення, ліве дерево, праве дерево. Рекурсивний генератор - це найпростіший спосіб відвідати всі вузли. Приклад:

tree = (0, (1, None, (2, (3, None, None), (4, (5, None, None), None))),
        (6, None, (7, (8, (9, None, None), None), None)))

def visit(tree):  # 
    if tree is not None:
        try:
            value, left, right = tree
        except ValueError:  # wrong number to unpack
            print("Bad tree:", tree)
        else:  # The following is one of 3 possible orders.
            yield from visit(left)
            yield value  # Put this first or last for different orders.
            yield from visit(right)

print(list(visit(tree)))

# prints nodes in the correct order for 'yield value' in the middle.
# [1, 3, 2, 5, 4, 0, 6, 9, 8, 7]

Змінити: замінити if treeна, if tree is not Noneщоб вловити інші помилкові значення як помилки.

Редагувати 2: про введення рекурсивних викликів у речення try: (коментар @ jpmc26).

Для поганих вузлів код вище просто реєструє ValueError і продовжує. Якщо, наприклад, (9,None,None)замінено на (9,None), результат буде

Bad tree: (9, None)
[1, 3, 2, 5, 4, 0, 6, 8, 7]

Більш типовим буде ререйз після реєстрації, що робить вихідним

Bad tree: (9, None)
Traceback (most recent call last):
  File "F:\Python\a\tem4.py", line 16, in <module>
    print(list(visit(tree)))
  File "F:\Python\a\tem4.py", line 14, in visit
    yield from visit(right)
  File "F:\Python\a\tem4.py", line 14, in visit
    yield from visit(right)
  File "F:\Python\a\tem4.py", line 12, in visit
    yield from visit(left)
  File "F:\Python\a\tem4.py", line 12, in visit
    yield from visit(left)
  File "F:\Python\a\tem4.py", line 7, in visit
    value, left, right = tree
ValueError: not enough values to unpack (expected 3, got 2)

Traceback дає шлях від кореня до поганого вузла. Можна було б обернути оригіналvisit(tree) виклик, щоб зменшити відстеження до шляху: (корінь, право, право, ліворуч, ліворуч).

Якщо рекурсивні виклики включені в речення try:, помилка повторно фіксується, реєструється та піднімається на кожному рівні дерева.

Bad tree: (9, None)
Bad tree: (8, (9, None), None)
Bad tree: (7, (8, (9, None), None), None)
Bad tree: (6, None, (7, (8, (9, None), None), None))
Bad tree: (0, (1, None, (2, (3, None, None), (4, (5, None, None), None))), (6, None, (7, (8, (9, None), None), None)))
Traceback (most recent call last):
...  # same as before

Кілька звітів про журналювання, швидше за все, більше шуму, ніж допомоги. Якщо хтось хоче шлях до поганого вузла, може бути найпростішим обернути кожен рекурсивний виклик у власну спробу: clause та підняти нову помилку ValueError на кожному рівні, до цього часу виконану контур.

Висновок: якщо ви не використовуєте виняток для управління потоком (як це може бути зроблено з IndexError, наприклад), наявність та розміщення спроб: оператори залежать від повідомлення про помилку, яке потрібно.


Я не бачу потреби в elseблоці на try/ except; було б простіше просто перенести цей код у tryблок, чи не так?
jpmc26,

6
Простіше? Так. Краще? Не на думку багатьох експертів, починаючи з GvR. python.org/dev/peps/pep-0008/#programming-reкомендації "Крім того, для всіх спроб / крім пропозицій обмежте пропозицію try абсолютною мінімальною кількістю необхідного коду. Знову ж це дозволяє уникнути маскування помилок."
Terry Jan Reedy

@ jpmc26 Див. редакцію 2 для обговорення вашого коментаря.
Terry Jan Reedy

1

До Python 3.4, функція генератора раніше мала викликати StopIterationвиняток, коли це робиться. Для рекурсивного випадку інші винятки (наприклад IndexError) виникають раніше StopIteration, тому ми додаємо їх вручну.

def recursive_generator(lis):
    if not lis: raise StopIteration
    yield lis[0]
    yield from recursive_generator(lis[1:])

for k in recursive_generator([6, 3, 9, 1]):
    print(k)

def recursive_generator(lis):
    if not lis: raise StopIteration
    yield lis.pop(0)
    yield from recursive_generator(lis)

for k in recursive_generator([6, 3, 9, 1]):
    print(k)

Зверніть увагу, що forцикл буде ловити StopIterationвиняток. Детальніше про це тут


1
Ви впевнені, що рекурсивний генератор не може просто нормально повернутися після завершення? Крім того, змінити введені дані на місці, як правило, потрібно уникати.
jpmc26,

1
@ jpmc26 на даний момент, так. Починаючи з 3.6, явне підвищення StopIteration всередині функції генератора є RuntimeError. Зазвичай просто повертаються. Дивіться python.org/dev/peps/pep-0479
Террі Ян Ріді

Насправді, починаючи з версії 3.5, явне підвищення StopIteration всередині функції генератора є застарілим cc: @TerryJanReedy. Отже, відповідь Левона - стара рекомендація до 3,4. Більшості з нас так і не подобалося писати явний StopIteration, це було непотрібно.
smci

1

Причиною того, що ваш рекурсивний виклик виконується лише один раз, є те, що ви по суті створюєте вкладені генератори. Тобто ви створюєте новий генератор усередині генератора кожного разу, коли ви рекурсивно викликаєте функцію rekurzivni_generator.

Спробуйте наступне, і ви побачите.

def recursive_generator(lis):
    yield lis[0]
    yield recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(type(k))

Одне просте рішення, як згадують інші, - це використання yield from.


0

Так, ви можете мати рекурсивні генератори. Однак вони страждають від тієї ж межі глибини рекурсії, що й інші рекурсивні функції.

def recurse(x):
  yield x
  yield from recurse(x)

for (i, x) in enumerate(recurse(5)):
  print(i, x)

Цей цикл досягає приблизно 3000 (для мене) перед збоєм.

Однак, з деякими хитрощами, ви можете створити функцію, яка подає генератор на себе. Це дозволяє писати генератори так, наче вони є рекурсивними, але не є такими: https://gist.github.com/3noch/7969f416d403ba3a54a788b113c204ce

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.