簡體   English   中英

Python 遞歸 function 超出遞歸限制。 如何將其轉換為迭代

[英]Python recursive function exceeds recursion limit. How can I convert it to iteration

我創建了一個 function 讀取 ID 對列表(即 [("A","B"),("B","C"),("C","D"),...] 和序列ID 從頭到尾包括任何分支。

Each list of ordered ID's is held in a class called an Alignment and this function uses recursion to handle branches by creating a new alignment starting at the ID at which the branch splits from the main list.

我發現對於某些輸入,可以達到 Python 設置的最大遞歸限制。 我知道我可以使用 sys.setrecursionlimit() 來增加這個限制,但是由於我不知道有多少分支組合是可能的,所以我想避免這種策略。

我已經閱讀了幾篇關於將遞歸函數轉換為迭代函數的文章,但我無法確定處理這個特定 function 的最佳方法,因為遞歸發生在 function 的中間並且可以是指數的。

你們中的任何人都可以提供任何建議嗎?

謝謝,布賴恩

代碼貼在下面:

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

編輯:我應該指出,提供的 ID 只是我用於測試此算法的一個小樣本。 實際上,ID 的序列可能長達數千,其中包含許多分支和分支的分支。

解決方案:感謝 Andrew Cooke。 新方法在調用堆棧上似乎更簡單、更容易。 我確實對他的代碼做了一些小的調整,以更好地適應我的目的。 我在下面包含了完整的解決方案:

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

更改摘要:交換鏈接和 have_successors 以創建從頭到尾的列表if line in known: known.remove(line)進行擴展,以便僅保留完整的系列將行變量從字符串更改為列表以處理多個字符在一個 ID 中。

更新:所以我剛剛發現我首先遇到所有這些問題的原因是對我提供的 ID 列表中的循環引用進行了處理。 既然循環引用是固定的,任何一種方法都可以按預期工作。 - 再次感謝你的幫助。

您的代碼是雜亂無章的混亂。 我無法詳細說明它應該做什么。 如果您更小心(更整潔、更清晰),那么您可能還會發現重構更容易。

無論如何,這可能會像你想要的那樣做:

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

我使用了 python2.7 - 對於早期版本,您可能需要將next(foo)替換為foo.__next__()或類似的。


關於編寫更簡潔的代碼

首先,我也是一個自學成才的程序員,最初是一名學者(天文學家),所以我很同情你。 如果你繼續前進,你可以趕上並超越許多“受過教育”的程序員。 這並不像你想象的那么難……

其次,使用像 defaultdict 這樣的“技巧”(這只是經驗/實踐的問題)和“整潔”之間是有區別的。 我不希望你知道 defaultdict - 這會隨着時間的推移而出現。

但是您現在應該能夠編寫干凈、簡單的代碼:

  • 我認為您有關於早期版本代碼的評論。 一個提到“最大長度”,但我沒有看到任何長度計算。 所以要么評論已經過時(在這種情況下為什么它在那里)或者它需要更清楚(為什么那些東西是最大長度的?)。 一般來說,您應該盡可能少地發表評論,否則它確實會過時。 但與此同時,您應該在不清楚代碼背后的“想法”的地方使用注釋。 代碼應該自己說話,所以不要說“我在這里添加兩個數字”,而是說“這里的片段必須是最大長度,因為......”如果有一些“隱藏”的邏輯。

  • 小心你使用的圖片。 由於某種原因,您的代碼以已知終端開頭。 所以你正在從頭到尾構建東西。 為什么? 這是看待問題的一種奇怪方式。 從開始但不是結束的點開始會不會更清楚? 然后使用“startIDs”來增長它們? 這樣你就“向前走”了。 這似乎是一件小事,但它使閱讀代碼變得混亂。

  • 為工作使用正確的工具。 您沒有使用 startID,那么為什么要構建 map? 你只需要一套。 也許您不知道集合,在這種情況下可以(但您現在知道:.o),但除此之外。 這也令人困惑 - 閱讀您的代碼的人希望您這樣做是有原因的。 所以當你做的比必要的多時,他們想知道為什么。

  • 避免在不需要時計算事物。 你有iindexcount 他們都需要嗎? 這些類型的計數器是最容易出現錯誤的方法,因為它們可能會出現愚蠢的小邏輯錯誤。 他們使代碼不清楚。 if i < count - 1:真的是在說“這是最后一個分支”嗎? 如果是這樣,寫if branch == branches [-1]:會更好,因為這很清楚你在想什么。

  • 與在 main 中循環對齊類似。 使用i只會使事情復雜化。 您正在處理每個 alignment,所以只需說for each alignment in alignments 如果由於對齊方式正在更改而導致錯誤,請制作不變的副本: for each alignment in list(alignments)

  • 如果不需要,請避免特殊情況。 在 buildAlignment 中,您在一開始就針對特殊情況進行了測試。 但它真的需要嗎? 沒有它你會得到同樣的結果嗎? 通常,當您簡單地編寫代碼時,事實證明您不需要特殊情況。 在我的代碼中,我不需要測試是否有一個或沒有“鏈接”,因為它在所有這些情況下都可以正常工作。 這給你更少的代碼和更少的擔心和更少的錯誤機會。

比所有這些事情更重要的是——你必須痴迷於整潔和有條不紊 你有很多想法,但不要嘗試一半然后跳到另一個,把它們寫下來並一個一個地完成它們。 否則你最終會得到一個你不理解的混亂和代碼。 起初感覺你在浪費時間,但你開始看到結果你變得越來越快,因為你花更少的時間感到困惑......


在發電機上

[我稍微修改了代碼以分隔newline並在幾個地方添加print 。]

首先,你運行代碼了嗎? 它在做你想做的事情嗎? 你能看到它與你以前的東西有什么聯系嗎? 我的expand類似於您的buildAlignment (我希望)。

如果你運行它(最新版本),你會看到:

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

這可能會為正在發生的事情提供線索。 “技巧”是 yield 語句 - python 編譯器看到了這一點,並且沒有制作普通的 function,而是制作了一個generator

發電機是一個很奇怪的東西。 它基本上是你的 function (在這種情況下, expand ),“捆綁”,以便它可以分階段運行。 運行由next()完成,每次達到yield時 function 再次停止。

所以trampoline是通過這個奇怪的捆綁。 它調用next() 這就是啟動function 的“魔法” function。 所以當next被稱為 function 開始運行,直到它到達yield ,它返回一個的包。 trampoline()命令然后保存舊包並開始處理新包,調用next() ,啟動它......等等。

當生成器“無事可做”時,它會引發StopIteration 因此,當我們到達表達式無法再增長的點時,我們會在trampoline()中看到該異常。 那時我們返回最后一個“舊”包(存儲在我們的stack中)並再次調用next() 這個包從它原來的位置重新啟動(就在yield之后)並繼續,可能在while中執行另一個循環,直到它再次達到yield (或用完並引發StopIteration )。

所以最后,代碼的作用與沒有yield一樣,唯一的區別是我們繼續制作這些捆綁包。 並歸還它們。 這似乎毫無意義。 除了我們不再使用堆棧! 因為返回了捆綁包,所以堆棧沒有被“用完”! 這就是為什么我們需要管理自己的堆棧(列表stack ) - 否則無法知道之前的調用是什么。

好吧,好吧,我不希望你明白這一點。 是的,這有點瘋狂。 現在你需要 go 和谷歌搜索“python 生成器”。 並編寫一些您自己的代碼來測試它。 但希望這指明了方向。


哦,我昨晚也在想。 我懷疑如果你用盡了堆棧,那實際上是因為你有循環,而不是因為鏈太長了。 你考慮過循環嗎? A->B, B->C, C->A, ....

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM