Können Generatoren rekursiv sein?

76

Ich habe naiv versucht, einen rekursiven Generator zu erstellen. Hat nicht funktioniert. Das habe ich getan:

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

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

Alles was ich bekam war der erste Gegenstand 6.

Gibt es eine Möglichkeit, solchen Code zum Laufen zu bringen? Im Wesentlichen den yieldBefehl in einem Rekursionsschema auf die darüber liegende Ebene übertragen?

Ein Mann
quelle
10
Sie geben nicht nach, wenn Sie es erneut anrufen. Es trifft die erste Rendite, sieht keine weitere Renditeerklärung und wird beendet.
Morgan Thrapp
2
Sie müssen entweder yield from another_generator()jedes Element einzeln oder explizit in der while-Schleife ausgeben. Ob another_generator()es in Ihrer Terminologie "rekursiv" ist oder nicht - das spielt keine Rolle.
Łukasz Rogalski
2
Mögliches Duplikat von Python: Wie man eine rekursive Generatorfunktion macht
Hashcode55

Antworten:

112

Versuche dies:

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

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

Ich sollte darauf hinweisen, dass dies aufgrund eines Fehlers in Ihrer Funktion nicht funktioniert. Es sollte wahrscheinlich einen Scheck enthalten, der lisnicht leer ist, wie unten gezeigt:

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

Wenn Sie Python 2.7 verwenden und dies nicht tun yield from, überprüfen Sie diese Frage.

Alec
quelle
26

Warum Ihr Code den Job nicht gemacht hat

In Ihrem Code ist die Generatorfunktion:

  1. gibt den ersten Wert der Liste zurück (ergibt)
  2. Anschließend wird ein neues Iteratorobjekt erstellt , das dieselbe Generatorfunktion aufruft und ihm einen Teil der Liste übergibt
  3. und hört dann auf

Die zweite Instanz des Iterators, die rekursiv erstellt wurde , wird niemals wiederholt. Deshalb haben Sie nur den ersten Punkt der Liste erhalten.

Eine Generatorfunktion ist nützlich, um automatisch ein Iteratorobjekt zu erstellen (ein Objekt, das das Iteratorprotokoll implementiert ), aber dann müssen Sie darüber iterieren: entweder manuell die next()Methode für das Objekt aufrufen oder mithilfe einer Schleifenanweisung, die das automatisch verwendet Iteratorprotokoll.

Können wir also rekursiv einen Generator aufrufen?

Die Antwort lautet ja . Zurück zu Ihrem Code: Wenn Sie dies wirklich mit einer Generatorfunktion tun möchten, können Sie wahrscheinlich Folgendes versuchen:

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)

Hinweis: Die Artikel werden in umgekehrter Reihenfolge zurückgegeben. Sie sollten sie daher verwenden, some_list.reverse()bevor Sie den Generator zum ersten Mal aufrufen.

In diesem Beispiel ist Folgendes zu beachten: Die Generatorfunktion ruft sich rekursiv in einer for- Schleife auf , die einen Iterator sieht und automatisch das Iterationsprotokoll verwendet, sodass tatsächlich Werte daraus abgerufen werden.

Das funktioniert, aber ich denke, das ist wirklich nicht nützlich . Wir verwenden eine Generatorfunktion, um eine Liste zu durchlaufen und die Elemente einzeln herauszuholen, aber ... eine Liste ist selbst iterierbar, sodass keine Generatoren erforderlich sind! Natürlich verstehe ich das, dies ist nur ein Beispiel, vielleicht gibt es nützliche Anwendungen dieser Idee.

Ein anderes Beispiel

Lassen Sie uns das vorherige Beispiel (für Faulheit) recyceln. Nehmen wir an, wir müssen die Elemente in einer Liste drucken und jedem Element die Anzahl der vorherigen Elemente hinzufügen (nur ein zufälliges Beispiel, das nicht unbedingt nützlich ist).

Der Code wäre:

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)

Wie Sie sehen, führt die Generatorfunktion tatsächlich etwas aus, bevor Listenelemente zurückgegeben werden, UND die Verwendung der Rekursion macht Sinn. Trotzdem nur ein dummes Beispiel, aber Sie bekommen die Idee.

Hinweis: Natürlich wird in diesem dummen Beispiel erwartet, dass die Liste nur Zahlen enthält. Wenn Sie wirklich versuchen möchten, es zu brechen, fügen Sie einfach einen String in some_list ein und haben Sie Spaß. Auch dies ist nur ein Beispiel, kein Produktionscode !

Daniele Barresi
quelle
Vielen Dank. Ich habe mich den ganzen Tag gefragt, warum Code sich geweigert hat, meinen Befehlen Folge zu leisten
Michael Iyke
13

Rekursive Generatoren sind nützlich, um nichtlineare Strukturen zu durchlaufen. Angenommen, ein Binärbaum ist entweder Keine oder ein Wertetupel, linker Baum, rechter Baum. Ein rekursiver Generator ist der einfachste Weg, alle Knoten zu besuchen. Beispiel:

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]

Bearbeiten: Ersetzen if treedurch if tree is not None, um andere falsche Werte als Fehler abzufangen.

Bearbeiten 2: Informationen zum Einfügen der rekursiven Aufrufe in die try: -Klausel (Kommentar von @ jpmc26).

Bei fehlerhaften Knoten protokolliert der obige Code nur den ValueError und fährt fort. Wenn zum Beispiel (9,None,None)durch ersetzt wird (9,None), ist die Ausgabe

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

Typischer wäre es, nach der Protokollierung erneut zu erhöhen, sodass die Ausgabe erfolgt

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)

Der Traceback gibt den Pfad von der Wurzel zum fehlerhaften Knoten an. Man könnte den ursprünglichen visit(tree)Aufruf umbrechen, um den Traceback auf den Pfad zu reduzieren: (root, right, right, left, left).

Wenn die rekursiven Aufrufe in der try: -Klausel enthalten sind, wird der Fehler auf jeder Ebene des Baums erneut erfasst, protokolliert und erneut ausgeführt.

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

Die mehreren Protokollierungsberichte sind wahrscheinlich mehr Rauschen als Hilfe. Wenn der Pfad zum fehlerhaften Knoten gewünscht wird, ist es möglicherweise am einfachsten, jeden rekursiven Aufruf in einer eigenen try: -Klausel zu verpacken und auf jeder Ebene einen neuen ValueError mit dem bisher erstellten Pfad auszulösen.

Schlussfolgerung: Wenn keine Ausnahme für die Flusskontrolle verwendet wird (wie dies beispielsweise bei IndexError der Fall ist), hängt das Vorhandensein und die Platzierung von try: -Anweisungen von der gewünschten Fehlerberichterstattung ab.

Terry Jan Reedy
quelle
Ich sehe keine Notwendigkeit für einen elseBlock auf dem try/ except; Es wäre einfacher, diesen Code einfach in den tryBlock zu verschieben, nicht wahr?
jpmc26
6
Einfacher? Ja. Besser? Nicht nach Meinung vieler Experten, beginnend mit GvR. python.org/dev/peps/pep-0008/#programming-recommendations " Beschränken Sie außerdem für alle try / without- Klauseln die try-Klausel auf die absolut erforderliche Mindestmenge an Code. Auch hier wird das Maskieren von Fehlern vermieden."
Terry Jan Reedy
@ jpmc26 Eine Diskussion Ihres Kommentars finden Sie unter Bearbeiten 2.
Terry Jan Reedy
1

Bis zu Python 3.4 musste eine Generatorfunktion eine StopIterationAusnahme auslösen, wenn sie fertig war. Für den rekursiven Fall werden andere Ausnahmen (z. B. IndexError) früher als ausgelöst StopIteration, daher fügen wir sie manuell hinzu.

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)

Beachten Sie, dass die forSchleife eine StopIterationAusnahme abfängt . Mehr dazu hier

Levon
quelle
1
Sind Sie sicher, dass ein rekursiver Generator nicht einfach normal zurückkehren kann, wenn er fertig ist? Das Ändern Ihrer Eingabe an Ort und Stelle ist im Allgemeinen etwas, das Sie vermeiden möchten.
jpmc26
1
@ jpmc26 derzeit ja. Ab 3.6 ist das explizite Erhöhen von StopIteration innerhalb einer Generatorfunktion ein RuntimeError. In der Regel kehren Sie einfach zurück. Siehe python.org/dev/peps/pep-0479
Terry Jan Reedy
Tatsächlich ist das explizite Erhöhen von StopIteration innerhalb einer Generatorfunktion seit 3.5 veraltet. Cc: @TerryJanReedy. Levons Antwort ist also eine alte Empfehlung bis 3.4. Die meisten von uns haben es sowieso nie gemocht, explizite StopIteration zu schreiben, es war unnötig.
smci
1

Der Grund, warum Ihr rekursiver Aufruf nur einmal ausgeführt wird, besteht darin, dass Sie im Wesentlichen verschachtelte Generatoren erstellen. Das heißt, Sie erstellen jedes Mal, wenn Sie die Funktion recursive_generator rekursiv aufrufen, einen neuen Generator in einem Generator.

Versuchen Sie Folgendes und Sie werden sehen.

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

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

Eine einfache Lösung ist, wie andere erwähnen, die Verwendung yield from.

Y Nehc
quelle
0

Ja, Sie können rekursive Generatoren haben. Sie leiden jedoch unter der gleichen Rekursionstiefengrenze wie andere rekursive Funktionen.

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

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

Diese Schleife erreicht vor dem Absturz ungefähr 3000 (für mich).

Mit einigen Tricks können Sie jedoch eine Funktion erstellen, die sich selbst einen Generator zuführt. Auf diese Weise können Sie Generatoren so schreiben, als wären sie rekursiv, jedoch nicht: https://gist.github.com/3noch/7969f416d403ba3a54a788b113c204ce

Elliot Cameron
quelle