递归python生成器:为什么需要迭代产量?

时间:2014-08-05 14:36:41

标签: python generator

测试一个人对递归的理解的一个很好的练习就是编写一个生成字符串所有排列的函数:

def get_perms(to_go, so_far=''):
    if not to_go:
        return [so_far]
    else:
        ret = []
        for i in range(len(to_go)):
            ret += get_perms(to_go[:i] + to_go[i+1:], so_far + to_go[i])
    return ret 

此代码很好,但我们可以通过使用生成器从内存的角度显着提高效率:

def perms_generator(to_go, so_far=''):
    if not to_go:
        yield so_far
    else:
        for i in range(len(to_go)):
            for perm in perms_generator(to_go[:i] + to_go[i+1:], so_far + to_go[i]):
                yield perm

(注意:最后的for循环也可以用python 3.3中的yield from替换。

我的问题:为什么我们需要迭代每个递归调用的结果?我知道yield返回一个生成器,但是从语句yield so_far看起来好像我们得到一个字符串,而不是我们需要迭代的东西。相反,似乎我们可以取代

for perm in perms_generator(to_go[:i] + to_go[i+1:], so_far + to_go[i]):
    yield perm

yield perms_generator(to_go[:i] + to_go[i+1:], so_far + to_go[i])

谢谢。如果标题不清楚,请告诉我。我有一种感觉,这个问题的内容与此SO question有关。

2 个答案:

答案 0 :(得分:3)

请记住,使用yield的任何函数都不会这些值返回给调用者。而是返回生成器对象,并且代码本身会暂停,直到您遍历生成器。每次遇到yield时,代码都会暂停:

>>> def pausing_generator():
...     print 'Top of the generator'
...     yield 1
...     print 'In the middle'
...     yield 2
...     print 'At the end'
... 
>>> gen = pausing_generator()
>>> gen
<generator object pausing_generator at 0x1081e0d70>
>>> next(gen)
Top of the generator
1
>>> next(gen)
In the middle
2
>>> next(gen)
At the end
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

调用pausing_generator()函数返回generator object。只迭代(在这里使用next()函数)运行函数中的实际代码,但每次遇到yield时暂停执行。

你的perms_generator函数返回一个这样的生成器对象,递归调用仍然返回一个生成器对象。您可以yield整个生成器对象,但是然后您生成一个生成生成器等的生成器,直到您到达最内层的生成器。

您可以使用print语句对此进行可视化:

>>> def countdown(i):
...     if not i:
...         return
...     yield i
...     recursive_result = countdown(i - 1)
...     print i, recursive_result
...     for recursive_elem in recursive_result:
...         yield recursive_elem
... 
>>> for i in countdown(5):
...     print i
... 
5
5 <generator object countdown at 0x1081e0e10>
4
4 <generator object countdown at 0x1081e0e60>
3
3 <generator object countdown at 0x1081e0eb0>
2
2 <generator object countdown at 0x1081e0f00>
1
1 <generator object countdown at 0x1081e0f50>

这里,递归调用返回了一个新的生成器对象;如果你想让生成器生成元素,你唯一的选择就是循环它并将元素向下移动,而不是生成器对象本身。

在Python 3中,您可以将yield from 委托用于嵌套生成器,包括递归调用:

def perms_generator(to_go, so_far=''):
    if not to_go:
        yield so_far
    else:
        for i, elem in enumerate(to_go):
            yield from perms_generator(to_go[:i] + to_go[i+1:], so_far + elem)

当遇到yield from迭代时,会继续进行递归调用,而不是产生整个生成器对象。

答案 1 :(得分:2)

returnyield之间的区别在于前者只返回一个值。后者意味着“将值包装在生成器中然后返回生成器。”

因此,在所有情况下,函数perms_generator()都会返回一个生成器。

表达式yield perms_generator()将再次将perms_generator()的结果包装在生成器中,为您提供生成器的生成器。这意味着功能将返回不同的东西;有时,它将是一个简单的生成器,有时是嵌套的生成器。对于您的代码的消费者而言,这将非常混乱。