生成器可以递归吗?


76

我天真地尝试创建一个递归生成器。没用 这是我所做的:

def recursive_generator(lis):
    yield lis[0]
    recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(k)

我所得到的只是第一项6

有没有办法使这种代码起作用?本质yield上是在递归方案中将命令转移到以上级别吗?


10
当您再次调用它时,您不会屈服。它达到第一个收益,没有看到另一个收益声明,然后退出。
Morgan Thrapp '16

2
您要么需要yield from another_generator(),要么在while循环中显式地逐个生成每个元素。another_generator()您的术语中是否“递归”-没关系。
卢卡斯Rogalski

Answers:


112

试试这个:

def recursive_generator(lis):
    yield lis[0]
    yield from recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(k)

我应该指出,由于您的功能存在错误,因此无法使用。它可能应该包含lis不为空的支票,如下所示:

def recursive_generator(lis):
    if lis:
        yield lis[0]
        yield from recursive_generator(lis[1:])

如果您使用的是Python 2.7而没有yield from请检查此问题。


26

为什么您的代码没有完成这项工作

在您的代码中,生成器函数:

  1. 返回(产生)列表的第一个值
  2. 然后创建一个新的迭代器对象,对象调用相同的生成器函数,并将列表的一部分传递给它
  3. 然后停下来

迭代器的第二个实例(递归创建的实例)永远不会被迭代。这就是为什么您只获得列表的第一项。

生成器函数对于自动创建迭代器对象(实现迭代器协议的对象)很有用,但是您需要对其进行迭代:手动next()在对象上调用方法,或者通过将自动使用迭代器协议。

那么,我们可以递归地调用生成器吗?

答案是肯定的。现在回到您的代码,如果您真的想使用生成器函数执行此操作,我想您可以尝试:

def recursive_generator(some_list):
    """
    Return some_list items, one at a time, recursively iterating over a slice of it... 
    """
    if len(some_list)>1:
    # some_list has more than one item, so iterate over it
        for i in recursive_generator(some_list[1:]):
            # recursively call this generator function to iterate over a slice of some_list.
            # return one item from the list.
            yield i
        else:
            # the iterator returned StopIteration, so the for loop is done.
            # to finish, return the only value not included in the slice we just iterated on.
            yield some_list[0]
    else:
        # some_list has only one item, no need to iterate on it.
        # just return the item.
        yield some_list[0]

some_list = [6,3,9,1]
for k in recursive_generator(some_list):
    print(k)

注意:这些项以相反的顺序返回,因此您可能需要some_list.reverse()在首次调用生成器之前使用。

在此示例中要注意的重要一点是:生成器函数在for循环中递归调用自身,该循环看到一个迭代器并自动对其使用迭代协议,因此它实际上从中获取值。

这行得通,但是我认为这真的没有用。我们使用生成器函数遍历一个列表,一次只取出一个项目,但是...列表本身是可迭代的,因此不需要生成器!我当然知道,这只是一个例子,也许这个想法有用。

另一个例子

让我们回收前面的示例(出于懒惰)。可以说,我们需要打印列表中的项目,并在每个项目中添加先前项目的计数(只是一个随机示例,不一定有用)。

该代码将是:

def recursive_generator(some_list):
    """
    Return some_list items, one at a time, recursively iterating over a slice of it...
    and adding to every item the count of previous items in the list
    """
    if len(some_list)>1:
    # some_list has more than one item, so iterate over it
        for i in recursive_generator(some_list[1:]):
            # recursively call this generator function to iterate over a slice of some_list.
            # return one item from the list, but add 1 first. 
            # Every recursive iteration will add 1, so we basically add the count of iterations.
            yield i + 1
        else:
            # the iterator returned StopIteration, so the for loop is done.
            # to finish, return the only value not included in the slice we just iterated on.
            yield some_list[0]
    else:
        # some_list has only one item, no need to iterate on it.
        # just return the item.
        yield some_list[0]

some_list = [6,3,9,1]
for k in recursive_generator(some_list):
    print(k)

现在,如您所见,生成器函数实际上在返回列表项之前已经做了一些事情,并且递归的使用开始变得有意义。不过,这只是一个愚蠢的例子,但您明白了。

注意:当然,在这个愚蠢的示例中,列表应仅包含数字。如果您真的想尝试打破它,只需在some_list中放入一个字符串即可,然后玩得开心。同样,这仅是示例,不是生产代码!


非常感谢你。一整天都想知道为什么代码拒绝服从我的命令
Michael Iyke

13

递归生成器对于遍历非线性结构很有用。例如,让二叉树为None或值元组,左树,右树。递归生成器是访问所有节点的最简单方法。例:

tree = (0, (1, None, (2, (3, None, None), (4, (5, None, None), None))),
        (6, None, (7, (8, (9, None, None), None), None)))

def visit(tree):  # 
    if tree is not None:
        try:
            value, left, right = tree
        except ValueError:  # wrong number to unpack
            print("Bad tree:", tree)
        else:  # The following is one of 3 possible orders.
            yield from visit(left)
            yield value  # Put this first or last for different orders.
            yield from visit(right)

print(list(visit(tree)))

# prints nodes in the correct order for 'yield value' in the middle.
# [1, 3, 2, 5, 4, 0, 6, 9, 8, 7]

编辑:替换if treeif tree is not None以捕获其他错误值作为错误。

编辑2:关于将递归调用放在try:子句中(@ jpmc26的注释)。

对于错误的节点,上面的代码仅记录ValueError并继续。例如,如果(9,None,None)被替换(9,None),则输出为

Bad tree: (9, None)
[1, 3, 2, 5, 4, 0, 6, 8, 7]

更典型的是在记录后重新筹集资金,使输出

Bad tree: (9, None)
Traceback (most recent call last):
  File "F:\Python\a\tem4.py", line 16, in <module>
    print(list(visit(tree)))
  File "F:\Python\a\tem4.py", line 14, in visit
    yield from visit(right)
  File "F:\Python\a\tem4.py", line 14, in visit
    yield from visit(right)
  File "F:\Python\a\tem4.py", line 12, in visit
    yield from visit(left)
  File "F:\Python\a\tem4.py", line 12, in visit
    yield from visit(left)
  File "F:\Python\a\tem4.py", line 7, in visit
    value, left, right = tree
ValueError: not enough values to unpack (expected 3, got 2)

追溯提供了从根到坏节点的路径。可以包装原始visit(tree)调用以减少对路径的追溯:(根,右,右,左,左)。

如果try:子句中包含递归调用,则会在树的每个级别上重新捕获,记录并重新引发错误。

Bad tree: (9, None)
Bad tree: (8, (9, None), None)
Bad tree: (7, (8, (9, None), None), None)
Bad tree: (6, None, (7, (8, (9, None), None), None))
Bad tree: (0, (1, None, (2, (3, None, None), (4, (5, None, None), None))), (6, None, (7, (8, (9, None), None), None)))
Traceback (most recent call last):
...  # same as before

多个日志报告可能比帮助更多。如果要到坏节点的路径,最简单的方法是将每个递归调用包装在自己的try:子句中,并在每个级别都提出一个新的ValueError,并使用到目前为止已构造的路径。

结论:如果未在流控制中使用异常(例如,可以通过IndexError完成),try:语句的存在和放置取决于所报告的错误。


我认为/不需要else块。将代码移入代码块会更简单,不是吗?tryexcepttry
jpmc26,2017年

6
更简单?是。更好?从GvR开始,并不是很多专家认为。 python.org/dev/peps/pep-0008/#programming-recommendations “此外,对于所有try / except子句,将try子句限制为绝对必要的最小代码量。同样,这避免了掩盖bug。”
Terry Jan Reedy

@ jpmc26有关您的评论的讨论,请参见“编辑2”。
Terry Jan Reedy

1

在Python 3.4之前,生成器函数通常必须在完成后引发StopIteration异常。对于递归情况,其他异常(例如IndexError)早于提出StopIteration,因此我们手动添加它。

def recursive_generator(lis):
    if not lis: raise StopIteration
    yield lis[0]
    yield from recursive_generator(lis[1:])

for k in recursive_generator([6, 3, 9, 1]):
    print(k)

def recursive_generator(lis):
    if not lis: raise StopIteration
    yield lis.pop(0)
    yield from recursive_generator(lis)

for k in recursive_generator([6, 3, 9, 1]):
    print(k)

请注意,for循环将捕获StopIteration异常。更多关于此这里


1
您确定递归生成器完成后不能仅仅正常返回吗?另外,通常要避免在适当位置修改输入。
jpmc26,2017年

1
@ jpmc26当前,是的。从3.6开始,显式提高生成器函数内部的StopIteration是RuntimeError。通常,只需返回即可。见python.org/dev/peps/pep-0479
Terry Jan Reedy

实际上,从3.5版本开始,不建议使用cc:@TerryJanReedy显式提高生成器函数内部的StopIteration。因此,Levon的答案是一个旧的建议,直到3.4。无论如何,我们大多数人都不喜欢编写显式的StopIteration,这是不必要的。
smci

1

递归调用仅执行一次的原因是,您实际上是在创建嵌套生成器。也就是说,每次递归调用函数recursive_generator时,您正在生成器内部创建一个新生成器。

尝试以下操作,您将看到。

def recursive_generator(lis):
    yield lis[0]
    yield recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(type(k))

像其他提到的那样,一种简单的解决方案是使用yield from


0

是的,您可以使用递归生成器。但是,它们具有与其他递归函数相同的递归深度限制。

def recurse(x):
  yield x
  yield from recurse(x)

for (i, x) in enumerate(recurse(5)):
  print(i, x)

在崩溃之前,此循环达到约3000(对我而言)。

但是,通过一些技巧,您可以创建一个将生成器馈入其自身的函数。这使您可以像递归生成器一样编写生成器,但不是递归生成器:https : //gist.github.com/3noch/7969f416d403ba3a54a788b113c204ce

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.