web-dev-qa-db-fra.com

Les générateurs peuvent-ils être récursifs?

J'ai naïvement essayé de créer un générateur récursif. N'a pas fonctionné C'est ce que j'ai fait:

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

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

Tout ce que j'ai eu était le premier article 6.

Existe-t-il un moyen de faire fonctionner ce code? Transférer essentiellement la commande yield au niveau supérieur dans un schéma de récurrence?

55
Aguy

Essaye ça:

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

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

Je dois signaler que cela ne fonctionne pas à cause d'un bogue dans votre fonction. Il devrait probablement inclure une vérification que lis n'est pas vide, comme indiqué ci-dessous:

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

Si vous êtes sur Python 2.7 et n'avez pas yield from, vérifier cette question.

81
Alec

Pourquoi votre code n'a pas fait le travail

Dans votre code, la fonction du générateur:

  1. renvoie (renvoie) la première valeur de la liste
  2. alors il crée un nouvel objet iterator appelant la même fonction de générateur, en lui passant une tranche de la liste
  3. et puis s'arrête

La deuxième instance de l'itérateur, celle créée récursivement, n'est jamais itérée. C'est pourquoi vous n'avez reçu que le premier élément de la liste.

Une fonction de générateur est utile pour créer automatiquement un objet itérateur (un objet qui implémente le protocole itérateur ), mais vous devez ensuite effectuer une itération dessus: soit manuellement appeler le next() méthode sur l'objet ou au moyen d'une instruction de boucle qui utilisera automatiquement le protocole itérateur.

Alors, pouvons-nous appeler récursivement un générateur?

La réponse est oui . Revenons maintenant à votre code, si vous vraiment voulez faire cela avec une fonction de générateur, je suppose que vous pourriez essayer:

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)

Remarque: les éléments sont renvoyés dans l'ordre inverse. Vous pouvez donc utiliser some_list.reverse() avant d'appeler le générateur pour la première fois.

La chose importante à noter dans cet exemple est: la fonction du générateur s’appelle de manière récursive dans une boucle pour, qui voit un itérateur et utilise automatiquement le protocole d’itération, de sorte qu’il en tire des valeurs.

Cela fonctionne, mais je pense que ce n'est vraiment pas utile . Nous utilisons une fonction génératrice pour parcourir une liste et extraire les éléments, un à la fois, mais ... une liste est elle-même un élément itérable, aucun générateur n'est donc nécessaire! Bien sûr, je comprends, ce n’est qu’un exemple, il existe peut-être des applications utiles de cette idée.

Un autre exemple

Reprenons l'exemple précédent (pour la paresse). Disons que nous devons imprimer les éléments d'une liste, en ajoutant à chaque élément le nombre d'éléments précédents (juste un exemple aléatoire, pas nécessairement utile).

Le code serait:

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)

Maintenant, comme vous pouvez le constater, la fonction de générateur fait quelque chose avant de renvoyer les éléments de la liste ET l’utilisation de la récursion commence à avoir un sens. Pourtant, juste un exemple stupide, mais vous avez l’idée.

Remarque: Bien sûr, dans cet exemple stupide, la liste ne devrait contenir que des chiffres. Si vous voulez vraiment essayer de le casser, insérez simplement une chaîne dans some_list et amusez-vous. Encore une fois, il ne s'agit que d'un exemple, pas de production code!

21
Daniele Barresi

Les générateurs récursifs sont utiles pour traverser des structures non linéaires. Par exemple, supposons qu'un arbre binaire soit Aucun ou un tuple de valeur, arbre gauche, arbre droit. Un générateur récursif est le moyen le plus simple de visiter tous les nœuds. Exemple:

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]

Edit: remplacez if tree Par if tree is not None Pour intercepter d'autres valeurs fausses sous forme d'erreurs.

Edit 2: pour mettre les appels récursifs dans la clause try: (commentaire de @ jpmc26).

Pour les noeuds incorrects, le code ci-dessus enregistre simplement ValueError et continue. Si, par exemple, (9,None,None) Est remplacé par (9,None), Le résultat est

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

Plus typique serait de relancer après la journalisation, rendant la sortie soit

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)

La traceback donne le chemin de la racine au mauvais noeud. On pourrait envelopper l'appel initial visit(tree) afin de réduire le traçage au chemin: (racine, droite, droite, gauche, gauche).

Si les appels récursifs sont inclus dans la clause try:, l'erreur est récupérée, relue de nouveau et relevée à chaque niveau de l'arborescence.

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

Les rapports de journalisation multiples sont probablement plus de bruit que d’aide. Si vous voulez le chemin d'accès au nœud défectueux, il peut être plus simple d'encapsuler chaque appel récursif dans sa propre clause try: et de générer une nouvelle valeur ValueError à chaque niveau, avec le chemin d'accès construit jusqu'à présent.

Conclusion: si on n'utilise pas d'exception pour le contrôle de flux (comme cela peut être fait avec IndexError, par exemple), la présence et les emplacements des instructions try: dépendent de la signalisation d'erreur souhaitée.

10
Terry Jan Reedy

Jusqu’à Python 3.4, une fonction du générateur devait auparavant déclencher une exception StopIteration. Ceci est également possible dans le cas récursif. (Par exemple, IndexError) élevé plus tôt que StopIteration, nous l’ajoutons donc manuellement.

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)

Notez que la boucle for attrapera l'exception StopIteration. Plus à ce sujet ici

1
Levon

Oui, vous pouvez avoir des générateurs récursifs. Cependant, ils souffrent de la même limite de profondeur de récursivité que d'autres fonctions récursives.

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

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

Cette boucle atteint environ 3000 (pour moi) avant de planter.

Cependant, avec quelques astuces, vous pouvez créer une fonction qui alimente un générateur. Cela vous permet d'écrire des générateurs comme s'ils étaient récursifs mais ne le sont pas: https://Gist.github.com/3noch/7969f416d403ba3a54a788b113c204ce

0
Elliot Cameron