我不时发现自己在Python中编写递归生成器。这是一个recent example:
def comb(input, lst = [], lset = set()):
if lst:
yield lst
for i, el in enumerate(input):
if lset.isdisjoint(el):
for out in comb(input[i+1:], lst + [el], lset | set(el)):
yield out
for c in comb([[1, 2, 3], [3, 6, 8], [4, 9], [6, 11]]):
print c
算法的细节并不重要。我将它作为一个完整的,真实的插图包含在内,为问题提供了一些背景信息。
我的问题是关于以下构造:
for out in comb(...):
yield out
这里,comb()
是生成器的递归实例化。
每次我必须拼出for: yield
循环时,它会让我感到畏缩。这是 在Python中编写递归生成器的方法,还是有更优越的(更惯用,更高性能等)替代方案?
答案 0 :(得分:8)
每次我必须拼出for:yield循环时,它会让我感到畏缩。这真的是在Python中编写递归生成器的方法,还是有更好的(更惯用,更高性能等)替代方案?
有一个更好的选择:
yield from comb(...)
这实际上与以下内容完全相同:
for out in comb(...):
yield out
这需要Python 3.3。如果你坚持使用Python 2.x(或者更老的3.x),你必须坚持使用旧方法,因为Python 2的语法在2.7之后永远不会再次更新(而3.0到3.2显然已经冻结了)。
首先,请参阅评论中提到的Wessie提到的pure Python yield from。这个版本只适用于单一级别的“yield from”,但是底部有一个链接到更灵活和优化(但更难理解)的版本。它实际上似乎没有用(我在NameError
上获得_stack
,但看起来应该很容易修复。如果是这样,并且如果可以接受放置@supergenerator
最外层发电机上的装饰器,如果性能可以接受,那就是你的答案。
如果没有,你可以采取各种技巧来处理多个级别的产量循环,而不是在每个级别。但是,它们都不会让你降到0级 - 实际上,它们很少值得做。例如:
一旦你从序列而不是生成器函数的角度思考,很明显我们所要做的就是将一个序列展平。无论你是试图压平N级,平坦直到达到不可迭代,变平直到满足其他一些可预测的等等,都有一个简单的算法;你必须选择正确的。但它会使你的代码更具惯用性,可读性,高性能等吗?很少。我们来看一个超级简单的案例。
def flatten(seq, levels=1):
for level in range(levels):
seq = itertools.chain.from_iterable(seq)
return seq
所以:
def a():
yield 1
yield 2
yield 3
def b():
yield a()
def c():
yield b()
def d():
yield c()
for i in flatten(d(), 3):
print i
好处是我只需要在一个地方,在呼叫站点,而不是在3个地方,在沿途的每个发电机处理嵌套。成本是不太明显的是读者会发生什么,更容易出错。 (好吧,在这种情况下不是那么多......但是想象一下扁平化直到lambda x: isinstance(list)
,测试地狱,释放它,然后有人在comb
上呼叫tuple
......比疾病更糟糕,这就是为什么我称它为伎俩。
除非压扁确实是算法的自然部分,否则某些中间步骤是您不能或不想触摸的代码,或者以这种方式构造事物是一个有用的说明或提醒某事,或......
只是为了好玩,我写了一首全能唱歌 - 所有你想要的功能,并将其作为补丁提交给Erik Rose的漂亮的more-itertools图书馆。即使他不接受它,你也可以在my fork中找到它 - 它被称为collapse
,它是文件中的最后一个函数。