Python: usando un algoritmo recursivo como generador

99

Recientemente escribí una función para generar ciertas secuencias con restricciones no triviales. El problema vino con una solución recursiva natural. Ahora sucede que, incluso para una entrada relativamente pequeña, las secuencias son varios miles, por lo que preferiría usar mi algoritmo como generador en lugar de usarlo para completar una lista con todas las secuencias.

Aquí hay un ejemplo. Supongamos que queremos calcular todas las permutaciones de una cadena con una función recursiva. El siguiente algoritmo ingenuo toma un argumento adicional 'almacenamiento' y le agrega una permutación cada vez que encuentra uno:

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

(No se preocupe por la ineficiencia, esto es solo un ejemplo).

Ahora quiero convertir mi función en un generador, es decir, producir una permutación en lugar de agregarla a la lista de almacenamiento:

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

Este código no funciona (la función se comporta como un generador vacío).

¿Me estoy perdiendo de algo? ¿Hay alguna manera de convertir el algoritmo recursivo anterior en un generador sin reemplazarlo por uno iterativo ?

Federico A. Ramponi
fuente

Respuestas:

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

O sin acumulador:

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
Markus Jarderot
fuente
29
En Python 3.4, puede reemplazar las dos últimas líneas con yield from getPermutations(string[:i] + string[i+1:]), ¡lo cual es más eficiente en muchos sentidos!
Manuel Ebert
1
Aún necesitaría generar el resultado de alguna manera. Usar yield fromrequeriría que uses el argumento del acumulador ( prefix).
Markus Jarderot
Sugerencia: Defina otro generador que devuelva string[i],string[:i]+string[i+1:]pares. Entonces sería:for letter,rest in first_letter_options(string): for perm in getPermuations(rest): yield letter+perm
Thomas Andrews
29

Esto evita la len(string)recursividad -deep y, en general, es una buena forma de manejar generadores dentro de generadores:

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

flattennos permite continuar el progreso en otro generador simplemente yieldingiriéndolo, en lugar de iterar a través de él y yieldrastrear cada elemento manualmente.


Python 3.3 se agregará yield froma la sintaxis, lo que permite la delegación natural a un subgenerador:

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])
efímero
fuente
20

La llamada interior para getPermutations, también es un generador.

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])  # <-----

Necesita iterar a través de eso con un bucle for (vea la publicación de @MizardX, ¡que me superó en segundos!)

S.Lott
fuente