Python递归函数超出递归限制,如何转换为迭代?
我创建了一个函数,可以读取一对对的ID列表,比如说[("A","B"),("B","C"),("C","D"),...],并且把这些ID从开始到结束按顺序排列,包括任何分支。
每一组有序的ID都保存在一个叫做Alignment的类里,这个函数使用递归来处理分支,也就是在分支从主列表分开的地方创建一个新的对齐。
我发现,有些输入会让Python达到最大递归限制。我知道可以用sys.setrecursionlimit()来增加这个限制,但因为我不知道可能有多少种分支组合,所以我想避免这样做。
我看了很多关于把递归函数转换成迭代函数的文章,但我还没找到处理这个特定函数的最佳方法,因为递归发生在函数的中间,并且可能是指数级的。
有没有人能给我一些建议呢?
谢谢,Brian
代码如下:
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)
以扩展,保留完整的序列
将line变量从字符串改为列表,以处理单个ID中的多个字符。
更新:我刚发现我最开始遇到问题的原因是因为我提供的ID列表中有循环引用。现在循环引用修复后,任一方法都能按预期工作。再次感谢大家的帮助。
1 个回答
你的代码看起来很乱,我看不出它具体想做什么。如果你能更仔细一点(让代码更整洁、更清晰),那么你可能会发现重构代码也会变得更容易。
不过,这段代码可能会做一些你想要的事情:
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")])
我使用的是Python 2.7,如果你用的是更早的版本,可能需要把next(foo)
换成foo.__next__()
或者类似的东西。
关于写更清晰的代码
首先,我也是自学成才的程序员,之前是学术界的(天文学家),所以我很理解你的处境。如果你继续努力,你会赶上并超过很多“受过教育”的程序员。这并没有你想象的那么难……
其次,使用像defaultdict这样的“技巧”是经验和练习的结果,而“整洁”是另一回事。我不指望你现在就知道defaultdict,这个知识会随着时间积累。
但你现在应该能做到的是写出干净、简单的代码:
我觉得你有些注释是关于代码早期版本的。其中一个提到“最大长度”,但我没看到任何长度计算。所以要么这个注释过时了(那它为什么还在呢),要么需要更清楚(为什么这些东西有最大长度?)。一般来说,你应该尽量少注释,因为注释容易过时。但在不清楚代码背后“想法”的地方,注释是必要的。代码应该能自我解释,所以不要说“我在这里加两个数字”,而要说“这里的片段必须是最大长度,因为……”如果有一些“隐藏”的逻辑。
使用的图示要小心。你的代码从已知的终端开始,这样你是从后往前构建的。为什么这样?这看起来很奇怪。难道从起点开始,而不是终点开始会更清楚吗?然后用“startIDs”来扩展它们?这样你就是“向前走”。这可能看起来是小事,但会让代码阅读起来很混乱。
使用合适的工具。你没有使用startIDs,为什么还要构建一个映射?你只需要一个集合。也许你不知道集合的用法,那没关系(但现在你知道了!:o)。否则,这样做也会让人困惑——读你代码的人会期待你是有原因地在做事情。所以当你做了不必要的事情时,他们会想知道为什么。
避免在不需要的时候计数。你有
i
、index
和count
,它们都必要吗?这些计数器最容易出错,因为它们可能会有一些小的逻辑错误。而且它们让代码变得不清晰。if i < count - 1:
真的在问“这是最后一个分支吗”?如果是的话,写成if branch == branches[-1]:
会更清楚。在主程序中循环对齐时也一样。使用
i
只会让事情复杂化。你在处理每个对齐,所以直接写for each alignment in alignments
。如果因为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编译器看到这个,就不会生成普通的函数,而是生成一个生成器。
生成器是个很奇怪的东西。它基本上是你的函数(在这个例子中是expand
),被“打包”起来,可以分阶段运行。运行是通过next()
来完成的,每当遇到yield
时,函数就会暂停。
所以trampoline
接收了这个奇怪的包裹。它调用next()
。这是“魔法”函数,它启动了这个函数。当调用next
时,函数开始运行,直到遇到yield
,在那时它返回一个新的包裹。然后trampoline()
命令保存旧的包裹,开始处理新的包裹,再次调用next()
,启动它……等等。
当生成器“没有事情可做”时,它会抛出StopIteration
。所以当我们到达一个表达式无法再增长的点时,我们会在trampoline()
中看到这个异常。此时我们返回到最后一个“旧”包裹(存储在我们的stack
中),再次调用next()
。这个包裹从它停下的地方重新开始(就在yield
之后),继续,可能在while
中再循环,直到再次遇到yield
(或者耗尽并抛出StopIteration
)。
所以最后,这段代码的效果和没有yield
是一样的!唯一的区别是我们不断地生成这些包裹,并返回它们。看起来似乎没有意义。除了我们不再使用栈!因为这个包裹是返回的,栈就不会被“耗尽”!这就是我们需要管理自己的栈(列表stack
)的原因——否则就无法知道上一个调用是什么。
好吧,我不指望你完全理解这一切。是的,这有点疯狂。现在你需要去谷歌搜索“python generators”,并写一些自己的代码来测试一下。但希望这能指引你方向。
哦,我昨晚还在想。如果你耗尽了栈,实际上是因为你有循环,而不是因为链条太长。你考虑过循环吗?A->B,B->C,C->A,……