2011-08-16 21 views
5

He creado una función que lee listas de pares de ID (es decir, [("A", "B"), ("B", "C"), ("C", "D" "), ...] y secuencia las ID desde el principio hasta el final, incluidas las ramas.La función recursiva de Python excede el límite de recursión. Cómo puedo convertirlo a la iteración

Cada lista de ID ordenados se almacena en una clase llamada Alineación y esta función usa la recursividad para manejar las ramas creando una nueva alineación comenzando en el ID en el que la rama se divide de la lista principal.

He encontrado que con ciertas entradas es posible alcanzar el límite máximo de recursión establecido por Python. Sé que podría aumentar este límite usando sys.setrecursionlimit (), pero como no sé cuántas combinaciones de ramas son posibles, me gustaría evitar esta táctica.

He estado leyendo varios artículos sobre la conversión de funciones recursivas a funciones iterativos, pero no han sido capaces de determinar la mejor manera de manejar esta función en particular debido a que la recursividad se lleva a cabo en medio de la función y puede ser exponencial.

¿Alguno de ustedes podría ofrecer alguna sugerencia?

Gracias, Brian

Código se publica a continuación:

def buildAlignments(alignment, alignmentList, endIDs): 
    while alignment.start in endIDs: 

     #If endID only has one preceding ID: add preceding ID to alignment 
     if len(endIDs[alignment.start]) == 1: 
      alignment.add(endIDs[alignment.start][0]) 

     else: 

      #List to hold all branches that end at spanEnd 
      branches = [] 

      for each in endIDs[alignment.start]: 

       #New alignment for each branch 
       al = Alignment(each) 

       #Recursively process each new alignment 
       buildAlignments(al, branches, endIDs) 

       branches.append(al) 
      count = len(branches) 
      i = 0 
      index = 0 

      #Loop through branches by length 
      for branch in branches: 
       if i < count - 1: 

        #Create copy of original alignment and add branch to alignment 
        al = Alignment(alignment) 
        al += branch #branches[index] 
        alignmentList.append(al) 
        i += 1 

       #Add single branch to existing original alignment 
       else: alignment += branch #branches[index] 
       index += 1 

def main(): 
    IDs = [("L", "G"), ("A", "B"), ("B", "I"), ("B", "H"), ("B", "C"), ("F", "G"), ("D", "E"), ("D", "J"), ("E", "L"), ("C", "D"), ("E", "F"), ("J", "K")] 

    #Gather all startIDs with corresponding endIDs and vice versa 
    startIDs = {} 
    endIDs = {} 
    for pair in IDs: 
     if not pair[0] in startIDs: startIDs[pair[0]] = [] 
     startIDs[pair[0]].append(pair[1]) 
     if not pair[1] in endIDs: endIDs[pair[1]] = [] 
     endIDs[pair[1]].append(pair[0]) 

    #Create Alignment objects from any endID that does not start another pair (i.e. final ID in sequence) 
    alignments = [Alignment(end) for end in endIDs if not end in startIDs] 

    #Build build sequences in each original Alignment 
    i = len(alignments) 
    while i: 
     buildAlignments(alignments[i-1], alignments, endIDs) 
     i -= 1 

EDIT: Debo señalar que las identificaciones proporcionadas son sólo una pequeña muestra de lo acostumbrado para probar este algoritmo. En realidad, las secuencias de identificaciones pueden ser de varios miles de largo con muchas ramas y ramas fuera de las ramas.

RESOLUCIÓN: Gracias a Andrew Cooke. El nuevo método parece ser mucho más simple y mucho más fácil en la pila de llamadas. Hice algunos ajustes menores a su código para adaptarme mejor a mis propósitos. He incluido la solución completa a continuación:

from collections import defaultdict 

def expand(line, have_successors, known): 
    #print line 
    known.append(line) 
    for child in have_successors[line[-1]]: 
     newline = line + [child] 
     if line in known: known.remove(line) 
     yield expand(newline, have_successors, known) 

def trampoline(generator): 
    stack = [generator] 
    while stack: 
     try: 
      generator = stack.pop() 
      child = next(generator) 
      stack.append(generator) 
      stack.append(child) 
     except StopIteration: 
      pass 

def main(pairs): 
    have_successors = defaultdict(lambda: set()) 
    links = set() 
    for (start, end) in pairs: 
     links.add(end) 
     have_successors[start].add(end) 
    known = [] 
    for node in set(have_successors.keys()): 
     if node not in links: 
      trampoline(expand([node], have_successors, known)) 
    for line in known: 
     print line 

if __name__ == '__main__': 
    main([("L", "G"), ("A", "B"), ("B", "I"), ("B", "H"), ("B", "C"), ("F", "G"), ("D", "E"), ("D", "J"), ("E", "L"), ("C", "D"), ("E", "F"), ("J", "K")]) 

RESUMEN DE CAMBIOS: enlaces intercambiados y have_successors para crear la lista de principio a fin añade if line in known: known.remove(line) para expandir el fin de conservar únicamente la variable de línea modificada serie completa de cuerdas a la lista para manejar múltiples caracteres en una sola ID.

ACTUALIZACIÓN: Por lo que acaba de descubrir la razón por la que estaba teniendo un problema con todo esto, en primer lugar es hacer a referencias circulares en la lista de identificadores que se proporcionó. Ahora que la referencia circular es fija, cualquiera de los métodos funciona como se espera. - Gracias, de nuevo, por toda su ayuda.

+0

Creo que quieres decir retoños. –

+0

¿Es este tu código real? Lanza IndexError en 'i = len (alineaciones)' ... 'buildAlignments (alineaciones [i]' incluso antes de que empiece a recursarse. –

+0

@Erin - gracias por señalarlo. He cambiado las rampas a las ramas, ya que una mejor descripción. – Brian

Respuesta

14

su código es un embrollo desorganizado. No puedo decir lo que se supone que debe hacer en detalle. si fueras más cuidadoso (más nítido, más claro), entonces probablemente también te resulte más fácil refactorizar.

todos modos, esto puede hacer algo como lo que quiere:

from collections import defaultdict 

def expand(line, links, known): 
    print 'expand' 
    known.append(line) 
    for child in links[line[-1]]: 
     newline = line + child 
     yield expand(newline, links, known) 

def trampoline(generator): 
    stack = [generator] 
    while stack: 
     try: 
      generator = stack.pop() 
      print 'next' 
      child = next(generator) 
      stack.append(generator) 
      stack.append(child) 
     except StopIteration: 
      pass 

def main(pairs): 
    have_successors = set() 
    links = defaultdict(lambda: set()) 
    for (start, end) in pairs: 
     have_successors.add(start) 
     links[end].add(start) 
    known = [] 
    for node in set(links.keys()): 
     if node not in have_successors: 
      trampoline(expand(node, links, known)) 
    for line in known: 
     print line 

if __name__ == '__main__': 
    main([("L", "G"), ("A", "B"), ("B", "I"), ("B", "H"), ("B", "C"), ("F", "G"), ("D", "E"), ("D", "J"), ("E", "L"), ("C", "D"), ("E", "F"), ("J", "K")]) 

i utilizado python2.7 - con versiones anteriores puede que tenga que reemplazar next(foo) con foo.__next__() o similar.


en la escritura de código más limpio

primero, yo también soy un programador autodidacta que comenzó como un académico (astrónomo), por lo que tiene mi simpatía. y si continúas, puedes ponerte al día y superar a muchos programadores "enseñados". no es tan difícil como podría pensar ...

segundo hay una diferencia entre utilizar "trucos" como defaultdict, que es solo una cuestión de experiencia/práctica, y ser "ordenado". No espero que sepas sobre el incumplimiento, eso vendrá con el tiempo.

pero lo que debe ser capaz de hacer ahora es escribir limpio, simple código:

  • creo que tiene algún comentario que están a punto versiones anteriores del código. uno menciona "longitud máxima" pero no veo ningún cálculo de longitud. entonces, o el comentario está desactualizado (en cuyo caso, ¿por qué está allí) o tiene que ser más claro (por qué son esas cosas de longitud máxima?). en general, debe comentar lo menos posible, porque de lo contrario se desactualiza. pero al mismo tiempo debe usar comentarios donde no está claro cuáles son las "ideas" detrás del código. el código debería hablar por sí mismo, así que no diga "estoy agregando dos números aquí", pero diga "los fragmentos aquí deben ser de longitud máxima porque ..." si hay alguna lógica "oculta".

  • tenga cuidado con las imágenes que utiliza. por algún motivo, su código comienza con terminales conocidos. entonces estás construyendo cosas desde el final hasta el comienzo. ¿por qué? esa es una manera extraña de ver el problema. ¿No sería más claro comenzar con los puntos que están en el inicio, pero no en el final? y luego usa "startIDs" para hacerlos crecer? de esa manera estás "caminando hacia adelante". puede parecer una cosita, pero hace que leer el código sea confuso.

  • utilice las herramientas adecuadas para el trabajo. no usaste los ID de inicio, entonces ¿por qué estás construyendo un mapa? todo lo que necesitabas era un juego. quizás usted no sabía acerca de los conjuntos, en cuyo caso OK (¡pero lo hace ahora!: o). pero de lo contrario, eso también es confuso: alguien que lea tu código espera que hagas cosas por alguna razón. entonces, cuando haces más de lo necesario, se preguntan por qué.

  • evite contar cosas cuando no lo necesite. tiene i y index y count. ¿son todos necesarios? Este tipo de contadores son la forma más fácil de tener errores, ya que pueden tener pequeños errores lógicos. y hacen que el código no sea claro es if i < count - 1: realmente diciendo "¿es esta la última rama"? si es así, sería mucho mejor escribir if branch == branches [-1]: porque eso es claro acerca de lo que estás pensando.

  • de forma similar con bucles sobre alineaciones en main. usar i solo complica las cosas. está procesando cada alineación, solo diga for each alignment in alignments. si eso da un error porque las alineaciones están cambiando, haga una copia inmutable: for each alignment in list(alignments).

  • evite los casos especiales si no son necesarios. En buildAlignment tienes una prueba desde el principio para un caso especial. pero, ¿realmente se necesitaba? habrías obtenido el mismo resultado sin eso? a menudo, cuando escribe el código simplemente, resulta que no necesita casos especiales. en mi código no necesito probar si hay uno o ningún "enlace" porque funciona bien en todos esos casos. esto le da menos código y menos cosas de qué preocuparse y menos posibilidades de errores.

más de todas estas cosas - tienes que estar obsesivamente ordenada y metódica. tiene muchas ideas, pero en lugar de probar la mitad de uno, salte a otro, escríbalos y resuélvalos uno a uno. de lo contrario, terminas con un desastre y un código que no entiendes. en un primer momento se siente como que está perdiendo el tiempo, pero que empiece a ver que como resultado se están volviendo más rápido debido a que pase menos tiempo confundido ...


en generadores

[modifiqué el código ligeramente para separar newline y para agregar print en un par de lugares.]

primero, ¿ejecutó el código? ¿Está haciendo el tipo de cosas que quieres? ¿Puedes ver cómo se conecta con lo que tenías antes? mi expand es similar a tu buildAlignment (espero).

si lo ejecuta (última versión) verás:

: python2.7 recurse.py 
next 
expand 
next 
expand 
next 
expand 
next 
expand 
next 
expand 
next 
expand 
next 
expand 
next 
next 
... 

que puede dar una idea de lo que está sucediendo. el "truco" es la declaración de rendimiento: el compilador de Python lo ve y, en lugar de realizar una función normal, crea un generador de .

un generador es algo muy extraño. Básicamente es su función (en este caso, expand), "incluida" para que pueda ejecutarse por etapas. la ejecución se realiza por next() y la función se detiene nuevamente cada vez que se alcanza yield.

así que trampoline se pasa este paquete extraño. y llama al next(). esa es la función "mágica" que comienza la función. por lo tanto, cuando se llama a next, la función comienza a ejecutarse, hasta que llega al yield, donde devuelve nuevo paquete. el comando trampoline() continuación, guarda el viejo paquete y empieza a trabajar en el nuevo, llamando next() en que, a partir de ella ..., etc, etc

cuando un generador "se queda sin cosas que hacer" plantea StopIteration. así que cuando alcanzamos un punto donde la expresión no puede crecer más, entonces vemos esa excepción en trampoline(). en ese punto volvemos al último paquete "viejo" (almacenado en nuestro stack) y llamamos al next() nuevamente. Este paquete reinicia desde donde estaba (justo después de yield) y continúa, probablemente haciendo otro ciclo en el while, hasta que llega a yield nuevamente (o se agota y aumenta StopIteration).

así que al final, el código hace lo mismo que si el yield no estuviera allí. la única diferencia es que seguimos haciendo estos paquetes y devolviéndolos. que parece inútil ¡excepto que ya no estamos usando la pila! porque el paquete es devuelto ¡la pila no se está "agotando"! es por eso que tenemos que gestionar nuestra propia pila (la lista stack); de lo contrario, no hay forma de saber cuál fue la llamada anterior.

ok, vale, no espero que lo comprendas del todo. sí, es una especie de loco.ahora necesita irse y buscar "generadores de pitón". y escribe tu propio código para probarlo. pero espero que esto indique el camino.


oh, también, estaba pensando anoche. y sospecho que si estabas agotando la pila, en realidad era porque tienes bucles, no porque las cadenas sean tan largas. ¿Has considerado los bucles? A-> B, B-> C, C-> A, ....

+0

Gracias por su respuesta. No estoy familiarizado con los comandos predeterminados, pero basados ​​en lo que acabo de leer en la documentación, suenan como la herramienta perfecta para contener todos los ID de terminación. ¿Podría explicar la función expandir, tengo problemas para entender qué representa la línea? Además, parece como si todavía estuvieras utilizando la recursividad en este código, simplemente se ha movido a un generador. De cualquier manera, este puede ser un buen comienzo, gracias. – Brian

+0

¿Estaría dispuesto a proporcionar comentarios sobre cómo organizar y limpiar mejor este código? Soy un programador autodidacta y admito que a veces tengo problemas para organizar mejor el código y comprender todas las mejores prácticas de estructuración de código. Gracias. – Brian

+0

Agregaré más a la respuesta porque el espacio disponible aquí es pequeño. Estoy a punto de hacer el desayuno, así que dame un tiempo ... –

Cuestiones relacionadas