递归生成器和send()

3

有人知道递归使用生成器时send()函数的工作原理吗?我原以为值会传递到当前生成器,然后再传递到递归生成器...但似乎不是这样的?以下是一些示例代码:

def Walk(obj):
  recurse = (yield obj)
  if not recurse:
    print 'stop recurse:', recurse
    return

  if isinstance(obj, list):
    print 'is list:', obj
    for item in obj:
      print 'item loop:', item
      walker = Walk(item)
      for x in walker:
        print 'item walk:', x
        recurse = (yield x)
        print 'item walk recurse:', recurse
        walker.send(recurse)

root = ['a', ['b.0', ['b.0.0']]]

walker = Walk(root)
for i, x in enumerate(walker):
  print i, x
  print 'send true'
  walker.send(True)

期望的输出应该是每个递归级别的每个值:
0 ['a', ['b.0', ['b.0.0']]]
1 'a'
2 ['b.0', ['b.0.0']]
3 'b.0'
4 ['b.0.0']
5 'b.0.0'

最终发生的情况是:
0 ['a', ['b.0', ['b.0.0']]]
send true
is list: ['a', ['b.0', ['b.0.0']]]
item loop: a
item walk: a
item walk recurse: None
stop recurse: None

似乎内部循环中的“recurse =(yield)”不等待发送值。或者说,不清楚内部循环“recurse”值如何变为“None”,因为调用者确实调用了“send()”。最终,目标基本上是递归地遍历树结构,但顶层调用者能够指定何时不递归进入子结构。例如:
walker = Walk(root)
for node in walker:
  if CriteriaMet(node):
    walker.send(True)
  else:
    walker.send(False)

1
还有其他人想起了(cdr(car(cdr(cdr(car(car lst)))))))吗? - cwallenpoole
1个回答

6
重要的是要意识到send()也会被消耗!
来自http://docs.python.org/reference/expressions.html#generator.send

恢复执行并“发送”一个值到生成器函数中。value参数将成为当前yield表达式的结果。send()方法返回生成器产生的下一个值,或者如果生成器在不产生其他值的情况下退出,则引发StopIteration异常。当调用send()启动生成器时,必须使用None作为参数进行调用,因为没有yield表达式可以接收该值。

这是您代码的快速重新编写,以使其按预期输出:

def Walk(obj):
  recurse = (yield obj)
  if not recurse:
    #print 'stop recurse:', recurse
    return

  if isinstance(obj, list):
    #print 'is list:', obj
    for item in obj:
      #print 'item loop:', item
      walker = Walk(item)

      recurse = None #first send must be None
      while True:
        try:
          x = walker.send(recurse)
        except StopIteration:
          break
        #print 'item walk:', x
        recurse = (yield x)
        #print 'item walk recurse:', recurse

root = ['a', ['b.0', ['b.0.0']]]

walker = Walk(root)
i = 0
x = walker.next()
while True:
  print i, x
  try:
    x = walker.send(True)
  except StopIteration:
    break
  i += 1

输出:

0 ['a', ['b.0', ['b.0.0']]]
1 a
2 ['b.0', ['b.0.0']]
3 b.0
4 ['b.0.0']
5 b.0.0

啊,天哪,不知道我怎么错过了那个。不过这真的很不幸——不得不使用 while True 而非 for...in,使得使用生成器的 send() 方法变得不太吸引人。 - Richard Levasseur
对于你正在做的事情,是的。生成器可以用于迭代或协程的开发。由于你想坚持使用迭代器协议,并且你的代码示例没有显示需要不断与协程通信,我确信你只需要指定是否在顶层递归一次(而不是在每次消耗后指定)。如果是这种情况,只需将其作为生成器的参数 - def walk(obj, recurse = True): ... - 在创建walk生成器时传递它一次即可。 - Jeremy Brown
实际上,我确实需要控制每个层次的递归。真正的用例是遍历对象层次结构以确定是否需要将数据保存到数据库(层次结构和条件都相当复杂)。通过传入一个谓词函数来控制递归可能会更清晰。 - Richard Levasseur
也许可以通过可变对象让协程共享状态?它们在单个上下文中相互切换,因此更改状态是安全的。shared_state = {"recurse" : True}; gen = walk(obj, shared_state); for i, x in enumerate(gen): ...; shared_state["recurse"] = False - Jeremy Brown
在Python 3中,将x = walker.next()替换为x = next(walker) - JdeHaan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接