繁体   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