5

我创建了一个读取 ID 对列表的函数(即 [("A","B"),("B","C"),("C","D"),...] 和序列ID 从头到尾包括任何分支。

每个有序 ID 的列表都保存在一个名为 Alignment 的类中,该函数使用递归来处理分支,方法是创建一个新的对齐,从分支从主列表拆分的 ID 开始。

我发现使用某些输入可以达到 Python 设置的最大递归限制。我知道我可以使用 sys.setrecursionlimit() 来增加这个限制,但是由于我不知道有多少分支组合是可能的,所以我想避免这种策略。

我已经阅读了几篇关于将递归函数转换为迭代函数的文章,但我无法确定处理这个特定函数的最佳方法,因为递归发生在函数的中间并且可能是指数的。

你们中的任何人都可以提供任何建议吗?

谢谢,布赖恩

代码贴在下面:

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 列表中的循环引用进行了处理。既然循环引用是固定的,任何一种方法都可以按预期工作。- 再次感谢你的帮助。

4

1 回答 1

15

您的代码是杂乱无章的混乱。我无法详细说明它应该做什么。如果您更小心(更整洁、更清晰),那么您可能还会发现重构更容易。

无论如何,这可能会像你想要的那样做:

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,那么您为什么要构建地图?你只需要一套。也许你不知道集合,在这种情况下可以(但你现在知道了!:o)。但除此之外,这也令人困惑——阅读您的代码的人希望您这样做是有原因的。所以当你做的比必要的多时,他们想知道为什么。

  • 避免在不需要时计算事物。你有iindexcount。他们都需要吗?这些类型的计数器是最容易出现错误的方法,因为它们可能会出现愚蠢的小逻辑错误。他们使代码不清楚。真的是if i < count - 1:在说“这是最后一个分支”吗?如果是这样,写起来会更好,if branch == branches [-1]:因为这很清楚你在想什么。

  • 与在 main 中循环对齐类似。使用i只是使事情复杂化。您正在处理每个对齐,所以只需说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 编译器看到了这一点,而不是制作一个普通的函数,而是制作了一个generator

发电机是一个很奇怪的东西。它基本上是你的函数(在这种情况下,expand),“捆绑”,以便它可以分阶段运行。运行由 完成,next()每次yield达到 a 时函数再次停止。

trampoline这个奇怪的捆绑包也是如此。它调用next(). 那是启动该功能的“魔术”功能。所以当next被调用时,函数开始运行,直到它到达yield,它返回一个的包。然后该trampoline()命令保存旧包并开始处理新包,调用next()它,启动它......等等。

当生成器“无事可做”时,它会引发StopIteration. 因此,当我们到达表达式不能再增长的地步时,我们会在trampoline(). 那时我们返回到最后一个“旧”包(存储在我们的 中stack)并next()再次调用它。这个捆绑包从它原来的位置重新启动yield(就在 之后)并继续,可能在 中进行另一个循环while,直到它yield再次命中(或用完并引发StopIteration)。

所以最后,代码就像yield不存在一样!唯一的区别是我们一直在制作这些捆绑包,然后将它们退回。这似乎毫无意义。除了我们不再使用堆栈!因为返回了捆绑包,所以堆栈没有被“用完”!这就是为什么我们需要管理自己的堆栈(列表stack) - 否则无法知道之前的调用是什么。

好吧,好吧,我不希望你明白这一点。是的,这有点疯狂。现在您需要离开并在 Google 上搜索“python 生成器”。并编写一些您自己的代码来测试它。但希望这指明了方向。


oh, also, i was thinking last night. and i suspect that if you were exhausting the stack it was actually because you have loops, not because the chains are so long. have you considered loops? A->B, B->C, C->A, ....

于 2011-08-16T23:54:38.357 回答