【问题标题】:Using generator send() within a for loop在 for 循环中使用生成器 send()
【发布时间】:2016-04-25 08:47:43
【问题描述】:

我将图遍历实现为生成器函数,它产生被访问的节点。

有时用户需要告诉遍历函数不应该遵循从特定节点传出的边;为了支持这一点,遍历检查返回给它的值(使用生成器send() 方法),如果是True,则将节点视为叶节点以进行遍历。

问题是最简单的用户循环有点长:

# simplified thanks to @tobias_k
# bfs is the traversal generator function
traversal = bfs(g, start_node)
try:
  n = next(traversal)
  while True:
    # process(n) returns True if don't want to follow edges out of n
    n = traversal.send(process(n))
except StopIteration:
    pass

有什么办法可以改善吗?

我认为这样的事情应该可行:

for n in bfs(g, start_node):
  ???.send(process(n))

但我觉得我缺少一些python语法的知识。

【问题讨论】:

  • 好吧,你可以通过将 try/except 放在循环之外来缩短它;这将为您节省一组 try/except 和 if 条件。
  • @tobias_k 已修复,谢谢。

标签: python python-3.x generator python-3.5


【解决方案1】:

我看不到在常规 for 循环中执行此操作的方法。但是,您可以创建另一个生成器,它迭代另一个生成器,使用一些“跟随函数”来确定是否跟随当前元素,从而将代码的棘手部分封装到一个单独的函数中。

def checking_generator(generator, follow_function):
    try:
      x = next(generator)
      while True:
        yield x
        x = generator.send(follow_function(x))
    except StopIteration:
        pass

for n in checking_generator(bfs(g, start_node), process):
    print(n)

【讨论】:

  • 这行得通!我猜唯一的缺点是,除了必须创建一个额外的效用函数之外,假设的???.send() 可以在循环中的许多地方使用,从而迫使循环继续。使用这种方法,只能在循环的最后发送值。太糟糕了,python 缺乏支持这种基本用例的语法。
  • @max 您可以通过保留对原始值的引用将额外值发送到生成器:traversal = bfs(g, start_node) ; for n in checking_generator(traversal,process): ... traversal.send(...) 尽管在这种情况下checking_generator 仍将根据它处理的最后一个节点进行处理。
  • @tobias_k 当生成函数结束时,它会引发一个 StopIteration。因此,将整个代码体包装在 try 中以抑制 StopIteration 并退出函数......然后引发 StopIteration 似乎有点愚蠢。 :)
  • 你必须明确处理StopIteration,否则RuntimeError可能会在from __future__ import generator_stop生效时发生。
  • @tobias_k 它是在 Python 3.5 中引入的。请参阅my answer中的 pep 和解决方法代码的链接
【解决方案2】:

我发现使用earlier version of PEP 342 中提出的扩展“继续”语句,我的问题会有一个单行答案:

for n in bfs(g, start_node):
  continue process(n)

然而,虽然 PEP 342 被接受,但在 Raymond 和 Guido 之间的this June 2005 discussion 之后,该特定功能被撤回:

雷蒙德·赫廷格说:

让我作为“继续 EXPR”的强 -1 记录下来。这 for-loop 是我们最基本的构造,在它的 现在的形式。 “继续”和“中断”也可以这样说 对人们来说具有接近零学习曲线的额外优势 从其他语言迁移。

任何使这些基本陈述复杂化的冲动都应该认真对待 仔细审查并坚持高标准的清晰度,可解释性, 明显性、有用性和必要性。 IMO,大多数都失败了 测试。

我不期待在教程中解释“继续 EXPR” 并认为它会作为反特征脱颖而出。

[...] 反对“继续 EXPR”的正确论点是 还没有用例;如果有一个好的用例,解释 很容易跟上。

圭多

如果 python 核心开发人员已经改变了他们对扩展“继续”的有用性的看法,也许这可以重新引入未来的 PEP。但是,鉴于已在引用的线程中讨论了与此问题中几乎相同的用例,并且没有发现有说服力,这似乎不太可能。

【讨论】:

  • 它可以是 generator.send_wait 或其他东西,而不是 continue generator。这将解决很多问题。
  • 问题在于不复杂的继续,而不是没有下一个发送的功能
【解决方案3】:

为了简化客户端代码,您可以使用普通的bsf() 生成器并检查其中的node.isleaf 属性:

 for node in bfs(g, start_node):
     node.isleaf = process(node) # don't follow if `process()` returns True

缺点是node 是可变的。或者你必须传递一个跟踪叶节点的共享数据结构:leaf[node] = process(node) 其中leaf 字典被传递到bfs() 之前。

如果你想显式使用.send() 方法;你必须处理StopIteration。见PEP 479 -- Change StopIteration handling inside generators。您可以将其隐藏在辅助函数中:

def traverse(tree_generator, visitor):
    try:
        node = next(tree_generator)
        while True:
             node = tree_generator.send(visitor(node))
    except StopIteration:
        pass

例子:

traverse(bfs(g, start_node), process)

【讨论】:

    【解决方案4】:

    我不认为这是一个常见的用例,将其视为原始生成器:

    def original_gen():
        for x in range(10):
            should_break = yield x
            if should_break:
                break
    

    如果should_break 的值总是基于x 的某个函数调用计算得出,那么为什么不直接编写生成器:

    def processing_gen(check_f):
        for x in range(10):
            yield x
            should_break = check_f(x)
            if should_break:
                break
    

    但是我通常认为处理生成的值的代码是写在循环中的(否则有循环的意义何在?)

    看起来你真正想要做的是创建一个生成器,其中调用__next__ 方法实际上意味着send(process(LAST_VALUE)) 可以用一个类来实现:

    class Followup_generator(): #feel free to use a better name
        def __init__(self,generator,following_function):
            self.gen = generator
            self.process_f = following_function
        def __iter__(self):
            return self
        def __next__(self):
            if hasattr(self,"last_value"):
                return self.send(self.process_f(self.last_value))
            else:
                self.last_value = next(self.gen)
                return self.last_value
        def send(self,arg):
            self.last_value = self.gen.send(arg)
            return self.last_value
        def __getattr__(self,attr):
            "forward other lookups to the generator (.throw etc.)"
            return getattr(self.gen, attr) 
    
    # call signature is the exact same as @tobias_k's checking_generator
    traversal = Followup_generator(bfs(g, start_node), process)
    for n in traversal: 
        print(n)
        n = traversal.send(DATA) #you'd be able to send extra values to it
    

    但是,这仍然没有被认为是经常使用的,我可以使用 while 循环,尽管我会将 .send 调用放在顶部:

    traversal = bfs(g, start_node)
    send_value = None
    while True:
        n = traversal.send(send_value)
        #code for loop, ending in calculating the next send_value
        send_value = process(n)
    

    您可以将其包装在 try: ... except StopIteration:pass 中,尽管我发现使用上下文管理器更好地表达只是等待错误引发:

    class Catch:
        def __init__(self,exc_type):
            if issubclass(exc_type,BaseException):
                self.catch_type = exc_type
            else:
                raise TypeError("can only catch Exceptions")
        def __enter__(self):
            return self
        def __exit__(self,exc_type,err, tb):
            if issubclass(exc_type, self.catch_type):
                self.err = err
                return True
    
    
    with Catch(StopIteration):
        traversal = bfs(g, start_node)
        send_value = None
        while True:
            n = traversal.send(send_value)
            #code for loop, ending in calculating the next send_value
            send_value = process(n)
    

    【讨论】:

      【解决方案5】:

      这可能是线程主题中问题的答案。

      看看traversal 函数和自定义send 函数中额外的空 yield 语句,它们完成了神奇的工作。

      # tested with Python 3.7
      
      def traversal(n):
          for i in range(n):
              yield i, '%s[%s] %s' % (' ' * (4 - n), n, i)
              stop = yield
              if stop:
                  yield  # here's the first part of the magic
              else:
                  yield  # the same as above
                  yield from traversal(int(n / 2))
      
      
      def send(generator, value):
          next(generator)   # here's the second part of the magic
          generator.send(value)
      
      
      g = traversal(4)
      
      for i, (num, msg) in enumerate(g):
          print('>', i, msg)
          stop = num % 2 == 0
          send(g, stop)
      

      【讨论】:

        【解决方案6】:

        我编写了一个small class SettableGenerator,它使用一种方法接收要发送的值,然后在调用__next__ 时将其转发给实际的生成器。

        你可以这样写:

        gen = SettableGenerator(bfs(g, start_node))
        for n in gen:
          gen.set(process(n))
        

        【讨论】:

          【解决方案7】:

          让我们考虑以下生成器。它生成从 0 到 9 的数字。对于每个生成的数字,它都会获取一个输入并将其存储到 ret

          def count_to_nine():
              # Output: numbers from 0 to 9
              # Input: converted numbers
              ret = []
              for i in range(10):
                  # Yield a number, get something back
                  val = (yield i)
                  # Remember that "something"
                  ret.append(val)
              return ret
          

          您确实可以使用next() + send() 对其进行迭代, 但最好的方法是单独使用send() 进行迭代

          g = count_to_nine()
          value = None  # to make sure that the first send() gives a None
          while True:
              value = g.send(value)  # send the previously generated value, get a new one
              value = f'#{value}'
          

          结果如下:

          StopIteration: ['#0', '#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8' , '#9']

          如果您想要该输出,请捕获 StopIteration 并从中获取结果。

          干杯!

          【讨论】:

            猜你喜欢
            • 2020-02-03
            • 2015-04-01
            • 1970-01-01
            • 1970-01-01
            • 2012-01-21
            • 2013-12-16
            • 1970-01-01
            • 2015-06-16
            • 1970-01-01
            相关资源
            最近更新 更多