2011-08-16 4 views
5

J'ai créé une fonction qui lit des listes de paires d'identité (par exemple [("A", "B"), ("B", "C"), ("C", "D "), ...] et séquence les identifiants du début à la fin, y compris les branchesLa fonction récursive Python dépasse la limite de récursivité. Comment puis-je convertir à l'itération

Chaque liste d'ID ordonnée est conservée dans une classe appelée Alignement et cette fonction utilise la récursivité pour gérer les branches en créant un nouveau départ à partir de l'alignement. J'ai trouvé que pour certaines entrées, il est possible d'atteindre la limite de récursion maximale définie par Python, je sais que je pourrais simplement augmenter cette limite en utilisant sys.setrecursionlimit (), mais comme je ne sais pas combien de combinaisons de branches sont possibles, j'aimerais éviter cette tactique:

J'ai lu plusieurs articles sur la conversion des fonctions récursives aux fonctions itératives, mais je n'ai pas été en mesure de déterminer la meilleure façon de gérer cette fonction particulière, car la récursion a lieu au milieu de la fonction et peut être exponentielle.

Pourrait-vous proposer des suggestions?

Merci, Code Brian

est affiché ci-dessous:

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: Je tiens à souligner que les ID fournis sont juste un petit échantillon j'ai utilisé pour tester cet algorithme. En réalité, les séquences d'ID peuvent être longues de plusieurs milliers avec de nombreuses branches et branches de branches.

RÉSOLUTION: Merci à Andrew Cooke. La nouvelle méthode semble être beaucoup plus simple et beaucoup plus facile sur la pile d'appels. J'ai fait quelques ajustements mineurs à son code pour mieux répondre à mes besoins. J'ai inclus la solution complétée ci-dessous:

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

RÉSUMÉ DES CHANGEMENTS: liens permutés et have_successors pour créer la liste du début à la fin ajouté if line in known: known.remove(line) se développer afin de ne conserver que la série complète variable ligne modifiée de chaîne énumérer afin de gérer plusieurs caractères dans un seul ID.

MISE À JOUR: Je viens de découvrir la raison pour laquelle j'avais un problème avec tout cela en premier lieu est de faire des références circulaires dans la liste des ID que je fournis. Maintenant que la référence circulaire est fixe, l'une ou l'autre méthode fonctionne comme prévu. - Merci encore pour votre aide.

+0

Je pense que vous voulez dire des ramifications. –

+0

Est-ce votre code actuel? Il lance IndexError à 'i = len (alignements)' ... 'buildAlignments (alignements [i]' avant même qu'il ne commence à être récursif.) –

+0

@Erin - merci de m'avoir signalé cela, j'ai changé les chutes en branches comme il est une meilleure description – Brian

Répondre

14

votre code est une pagaille désorganisée. Je ne peux pas dire ce qu'il est censé faire en détail. Si vous étiez plus prudent (plus propre, plus clair), vous auriez probablement aussi plus de facilité à refactoriser.

de toute façon, cela peut faire quelque chose comme ce que vous voulez:

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 utilisé python2.7 - avec les versions précédentes, vous devrez peut-être remplacer next(foo) avec foo.__next__() ou similaire.


sur l'écriture de code plus propre

d'abord, je suis trop un programmeur autodidacte qui a commencé comme un universitaire (astronome), vous avez ma sympathie. et si vous continuez, vous pouvez rattraper et passer de nombreux programmeurs "enseignés". il n'est pas aussi difficile que vous pourriez le penser ...

deuxième il ya une différence entre l'utilisation de "trucs" comme defaultdict, qui est juste une question d'expérience/de pratique, et d'être "bien rangé". Je ne m'attends pas à ce que vous sachiez à propos de defaultdict - cela viendra avec le temps.

mais ce que vous devriez être en mesure de faire maintenant est propre écriture, simple code:

  • je pense que vous avez des commentaires qui sont sur les versions antérieures du code. on mentionne "longueur max" mais je ne vois pas de calculs de longueur. Donc, soit le commentaire est périmé (dans ce cas, pourquoi est-il là) ou il doit être plus clair (pourquoi sont ces choses de longueur max?). En général, vous devriez commenter le moins possible, car sinon, il ne sera plus à jour. mais en même temps, vous devriez utiliser des commentaires où il n'est pas clair ce que les «idées» sont derrière le code. le code devrait parler d'elle-même, alors ne dites pas "j'ajoute deux nombres ici", mais dites "les fragments ici doivent être de longueur maximum parce que ..." s'il y a une logique "cachée".

  • Soyez prudent avec les images que vous utilisez. Pour une raison quelconque, votre code commence par des terminaux connus. donc vous construisez des choses de la fin vers le début. Pourquoi? C'est une façon étrange de regarder le problème. ne serait-il pas plus clair de commencer avec des points en début, mais pas en fin? puis utilisez "startIDs" pour les développer? De cette façon, vous «avancez». cela peut sembler une petite chose, mais cela rend la lecture du code déroutante.

  • utilisez les bons outils pour ce travail. vous n'avez pas utilisé startID, alors pourquoi construisez-vous une carte? tout ce dont vous aviez besoin était un ensemble. Peut-être que vous ne saviez pas sur les ensembles, dans ce cas OK (mais vous faites maintenant!: o). mais sinon, cela aussi est déroutant - quelqu'un qui lit votre code s'attend à ce que vous fassiez des choses pour une raison. alors quand vous faites plus que nécessaire, ils se demandent pourquoi. Évitez de compter les choses quand vous n'en avez pas besoin. vous avez i et index et count. Sont-ils tous nécessaires? ces types de compteurs sont le moyen le plus facile d'avoir des bogues, car ils peuvent avoir de petites erreurs logiques. et ils rendent le code peu clair. est if i < count - 1: disant vraiment "est-ce la dernière branche"? Si c'est le cas, il serait beaucoup plus agréable d'écrire if branch == branches [-1]: parce que c'est effacer sur ce que vous pensez.

  • de la même manière avec des alignements en boucle sur le principal. en utilisant i ne fait que compliquer les choses. vous traitez chaque alignement, alors dites simplement for each alignment in alignments. Si cela donne une erreur parce que les alignements changent, faites une copie immuable: for each alignment in list(alignments).

  • éviter des cas particuliers si elles ne sont pas nécessaires. Dans buildAlignment, vous avez un test dès le départ pour un cas particulier. mais était-ce vraiment nécessaire? auriez-vous obtenu le même résultat sans cela? souvent, lorsque vous écrivez le code simplement, il s'avère que vous n'avez pas besoin de cas particuliers. dans mon code je n'ai pas besoin de tester s'il y a un ou pas de "liens" car cela fonctionne bien dans tous ces cas. cela vous donne moins de code et moins de choses à s'inquiéter et moins de chance pour les bugs.

plus de toutes ces choses - vous devez être obsessionnelle bien rangé et méthodique. vous avez beaucoup d'idées, mais plutôt que d'essayer la moitié d'un, puis de sauter à l'autre, de les écrire et de les parcourir un par un. sinon vous vous retrouvez avec un désordre et un code que vous ne comprenez pas. Au début, il se sent comme vous perdez votre temps, mais vous commencez à voir que par conséquent vous sont de plus en plus vite parce que vous passez moins de temps confus ...


sur les générateurs

[i modifié le code légèrement pour séparer newline et ajouter print dans un couple d'endroits.]

d'abord, avez-vous exécuté le code? fait-il le genre de chose que vous voulez? pouvez-vous voir comment cela se connecte avec ce que vous aviez avant? mon expand est similaire à votre buildAlignment (j'espère).

si vous l'exécutez (dernière version), vous verrez:

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

qui peut donner la moindre idée de ce qui se passe. le "truc" est l'instruction de rendement - le compilateur python voit cela et, au lieu de faire une fonction ordinaire, crée un générateur .

un générateur est une chose très étrange. c'est essentiellement votre fonction (dans ce cas, expand), "empaqueté" de sorte qu'il peut être exécuté par étapes. l'exécution est effectuée par next() et la fonction s'arrête à chaque fois qu'un yield est atteint.

donc trampoline est passé ce paquet étrange. et il appelle next(). c'est la fonction "magique" qui commence la fonction. donc quand next est appelé la fonction commence à courir, jusqu'à ce qu'il atteigne le yield, où il renvoie un nouveau lot. la commande trampoline() enregistre le vieux paquet et commence à travailler sur la nouvelle, appelant next() là-dessus, le démarrer ... etc etc

lorsqu'un générateur « à court de choses à faire », il soulève StopIteration. donc quand nous atteignons un point où l'expression ne peut plus grandir, nous voyons cette exception dans trampoline(). à ce moment-là, nous retournons au dernier «ancien» paquet (enregistré dans notre stack) et appelons à nouveau le next() là-dessus. ce lot redémarre d'où il était (juste après yield) et continue, faisant probablement une autre boucle dans le while, jusqu'à ce qu'il atteigne à nouveau yield (ou s'épuise et lève StopIteration). Donc, à la fin, le code fait la même chose que si le yield n'était pas là! la seule différence est que nous continuons à faire ces paquets et à les renvoyer. ce qui semble inutile. sauf que nous n'utilisons plus la pile! parce que le paquet est retourné la pile n'est pas "épuisée"! c'est pourquoi nous devons gérer notre propre pile (la liste stack) - sinon il n'y a aucun moyen de savoir quel était l'appel précédent.

ok, ok, je ne m'attends pas à ce que vous compreniez cela du tout. oui, c'est un peu fou.Maintenant, vous devez partir et google pour "générateurs python". et écrivez votre propre code pour le tester. mais j'espère que cela montre le chemin.


oh, aussi, je pensais hier soir. et je soupçonne que si tu épuisais la pile c'était en fait parce que tu as des boucles, pas parce que les chaînes sont si longues. avez-vous considéré les boucles? A-> B, B-> C, C-> A, ....

+0

Merci pour votre réponse. Je ne suis pas familier avec defaultdicts, mais basé sur ce que je viens de lire dans la documentation, ils sonnent comme l'outil parfait pour contenir tous les endIDs. Pourriez-vous s'il vous plaît expliquer la fonction d'expansion, j'ai du mal à comprendre ce que la ligne représente? En outre, il semble que vous utilisez encore la récursivité dans ce code, il vient d'être déplacé dans un générateur. Quoi qu'il en soit, cela peut être un bon début - merci. – Brian

+0

Seriez-vous prêt à donner votre avis sur la meilleure façon d'organiser et de nettoyer ce code? Je suis un programmeur autodidacte et j'avoue avoir parfois du mal à organiser au mieux le code et à comprendre toutes les bonnes pratiques de structuration du code. Je vous remercie. – Brian

+0

J'ajouterai plus à la réponse parce que l'espace disponible ici est petit. Je suis sur le point de faire le petit déjeuner, alors donnez-moi un moment ... –

Questions connexes