【问题标题】:Structured programming and Python generators?结构化编程和 Python 生成器?
【发布时间】:2010-12-17 06:00:06
【问题描述】:

更新:我一直想要的是greenlets


注意:当人们回答并迫使我“提高赌注”时,这个问题发生了一些变化,因为我的琐碎示例进行了琐碎的简化;根据 Alex 的建议,我不会在此处继续对其进行变异,而是在头脑更清晰时提出这个问题。


Python 生成器很漂亮,但我怎样才能轻松地将其分解为模块(结构化编程)?我实际上想要PEP 380,或者至少在语法负担上相当的东西,但在现有的Python(例如2.6)中

作为一个(诚然愚蠢的)例子,采取以下方式:

def sillyGenerator():
  for i in xrange(10):
    yield i*i
  for i in xrange(12):
    yield i*i
  for i in xrange(8):
    yield i*i

作为 DRY 的忠实信徒,我在这里发现了重复的模式并将其分解为一种方法:

def quadraticRange(n):
  for i in xrange(n)
    yield i*i

def sillyGenerator():
  quadraticRange(10)
  quadraticRange(12)
  quadraticRange(8)

...这当然行不通。父级必须在循环中调用新函数,产生结果:

def sillyGenerator():
  for i in quadraticRange(10):
    yield i
  for i in quadraticRange(12):
    yield i
  for i in quadraticRange(8):
    yield i

...比以前更长了!

如果我想将生成器的一部分推送到函数中,我总是需要这个相当冗长的两行包装器来调用它。如果我想支持 send(),那就更糟了:

def sillyGeneratorRevisited():
  g = subgenerator()
  v = None
  try:
    while True:
      v = yield g.send(v)
  catch StopIteration:
    pass
  if v < 4:
    # ...
  else:
    # ...

这还没有考虑传入异常。每次都是相同的样板!然而,一个人不能应用 DRY 并将这个相同的代码分解为一个函数,因为......你需要样板来调用它!我想要的是这样的:

def sillyGenerator():
  yield from quadraticRange(10)
  yield from quadraticRange(12)
  yield from quadraticRange(8)

def sillyGeneratorRevisited():
  v = yield from subgenerator()
  if v < 4:
    # ...
  else:
    # ...

有没有人能解决这个问题?我有第一次尝试,但我想知道其他人想出了什么。最终,任何解决方案都必须解决主生成器根据发送到生成器的数据结果执行复杂逻辑的示例,并可能对子生成器进行大量调用:我的用例是用于实现的生成器长时间运行的复杂状态机。

【问题讨论】:

  • 你能不能简化你真正的问题(不要太多,足够了!)而不是编造一个看起来不太匹配的问题?
  • 示例:并非如此。我只想知道如何在没有样板文件的情况下分解复杂的生成器——类似于下面描述的“yield from”语句,但在现有的 Python 中。满足简单情况的技巧是不够的:(

标签: python generator


【解决方案1】:
import itertools

def quadraticRange(n):
  for i in xrange(n)
    yield i*i

def sillyGenerator():
  return itertools.chain(
    quadraticRange(10),
    quadraticRange(12),
    quadraticRange(8),
  )

def sillyGenerator2():
  return itertools.chain.from_iterable(
    quadraticRange(n) for n in [10, 12, 8])

如果您想确保一个迭代器在另一个迭代器启动之前耗尽(包括它的初始化代码),最后一个很有用。

【讨论】:

  • +1 不错。我喜欢这种解决 99% 用例的方式(无限的生成器集,昂贵的生成器构造。)但是,我真的在考虑像协程这样的用例,其中顶级函数中的逻辑很复杂,并且取决于关于调用者发送的内容。我的例子被证明太琐碎了:(
【解决方案2】:

你想chain several iterators一起:

from itertools import chain

def sillyGenerator(a,b,c):
    return chain(quadraticRange(a),quadraticRange(b),quadraticRange(c))

【讨论】:

  • 不错。对于像我最初提出的那些更简单的例子,当然可以解决这个问题。然而,这迫使我扩展我的问题,因为我真正关注的问题有围绕启动发电机的控制逻辑:(
  • def sillyGenerator(*args): return chain(*map(quadraticRange, args)) 避免重复:)
【解决方案3】:

对于quadraticRange的任意数量的调用:

from itertools import chain

def sillyGenerator(*args):
    return chain(*map(quadraticRange, args))

此代码使用mapitertools.chain。它接受任意数量的参数并将它们传递给quadraticRange。然后将生成的迭代器链接起来。

【讨论】:

    【解决方案4】:

    有一个 Python 增强提案为“委托代”提供yield from statement。你的例子可以写成:

    def sillyGenerator():
      sq = lambda i: i * i
      yield from map(sq, xrange(10))
      yield from map(sq, xrange(12))
      yield from map(sq, xrange(8))
    

    或者更好,本着 DRY 的精神:

    def sillyGenerator():
      for i in [10, 12, 8]:
        yield from quadraticRange(i)
    

    该提案处于草稿状态,最终是否包含在内尚不确定,但它表明其他开发人员分享了您对生成器的想法。

    【讨论】:

    • 当我在查找参考语法时,你刚刚打败了我:)
    【解决方案5】:

    不切实际(不幸)的答案:

    from __future__ import PEP0380
    
    def sillyGenerator():
        yield from quadraticRange(10)
        yield from quadraticRange(12)
        yield from quadraticRange(8)
    

    潜在实用参考:Syntax for delegating to a subgenerator

    不幸的是,这不切实际:Python language moratorium

    更新 2011 年 2 月:

    暂停已解除,PEP 380 在 Python 3.3 的 TODO 列表中。希望这个答案很快就会实用。

    阅读Guido's remarks on comp.python.devel

    【讨论】:

    • +1 以获得有趣和良好的链接。坚持一些更直接的进口虽然:)
    • 请注意,我并不提倡使用它,但对于好奇的 pep380 可以在纯 Python 中实现,只需进行一些修改以使其在语法上有效:yield _from(quadraticRange(10))。 code.activestate.com/recipes/…
    【解决方案6】:

    但是,我想让我的 可重用性标准更难一个档次: 如果我需要一个控制结构怎么办 围绕着我重复的一代?

    itertools 即使在那里也经常有帮助 - 你需要提供你认为没有用的具体例子。

    对于 例如,我可能想打电话给 永远不同的子生成器 参数。

    itertools.chain.from_iterable.

    或者我的子生成器可能 非常昂贵,我只想 启动它们 到达。

    chainchain_from_iterable 都这样做 - 直到需要其中的第一个项目时,才会“启动”子迭代器。

    或者(这是一个真实的 愿望)我可能想改变我的工作 接下来基于我的控制器 使用 send() 传递给我。

    一个具体的例子将不胜感激。无论如何,最坏的情况是,您将编码 for x in blargh: yield x,而暂停的 Pep3080 会让您编码 yield from blargh——大约多出 4 个字符(不是悲剧;-)。

    如果某些 itertools 功能的一些复杂的协程版本(itertools 主要支持迭代器 - 还没有等效的 coroutools 模块)变得有保证,因为在您的代码中经常重复某种协程组合模式,那么它并不难自己编码。

    例如,假设我们经常发现自己在做这样的事情:首先产生某个值;然后,重复地,如果我们被发送'foo',则从 fooiter 产生下一个项目,如果 'bla',来自 blaiter,如果'zop',来自 zopiter,其他任何东西,来自 defiter。一旦我们发现第二次出现这种组合模式,我们就可以编写代码:

    def corou_chaiters(initsend, defiter, val2itermap):
      currentiter = iter([initsend])
      while True:
        val = yield next(currentiter)
        currentiter = val2itermap(val, defiter)
    

    并在需要时调用这个简单的组合函数。如果我们需要组合其他协程,而不是一般的迭代器,我们将使用 send 方法而不是 next 内置函数有一个稍微不同的组合器;等等。

    如果你能提供一个不容易被这些技术驯服的例子,我建议你在一个单独的问题中这样做(专门针对类似协程的生成器),因为已经有很多关于这个的材料很少与您的其他更复杂/复杂的示例有关。

    【讨论】:

      【解决方案7】:

      有一种我称之为“生成器内核”的模式,其中生成器不直接向用户屈服,而是向一些“内核”循环,将其(部分)产量视为具有特殊含义的“系统调用”。

      您可以通过一个中间函数在此处应用它,该函数接受生成的生成器并自动展开它们。为了方便使用,我们将在装饰器中创建该中间函数:

      import functools, types
      
      def flatten(values_or_generators):
          for x in values_or_generators:
              if isinstance(x, GeneratorType):
                  for y in x:
                      yield y
              else:
                  yield x
      
      # Better name anyone?
      def subgenerator(g):
          """Decorator making ``yield <gen>`` mean ``yield from <gen>``."""
      
          @functools.wraps(g)
          def flat_g(*args, **kw):
              return flatten(g(*args, **kw))
          return flat_g
      

      然后你就可以写了:

      def quadraticRange(n):
          for i in xrange(n)
              yield i*i
      
      @subgenerator
      def sillyGenerator():
          yield quadraticRange(10)
          yield quadraticRange(12)
          yield quadraticRange(8)
      

      请注意,subgenerator() 正好展开层次结构的一层。您可以轻松地将其设置为多级(通过管理手动堆栈,或者只是用for y in flatten(x): 替换内部循环 - 但我认为这样更好,这样每个想要使用这种非标准语法的生成器都必须用@subgenerator 显式包装。

      还要注意,生成器的检测是不完善的!它会检测写成生成器的东西,但这是一个实现细节。作为生成器的调用者,您所关心的只是它返回一个迭代器。它可能是一个返回一些 itertools 对象的函数,然后这个装饰器就会失败。

      检查对象是否具有.next() 方法过于宽泛——如果不将它们分开,您将无法生成字符串。所以最可靠的方法是检查一些明确的标记,所以你会写例如:

      @subgenerator
      def sillyGenerator():
          yield 'from', quadraticRange(10)
          yield 'from', quadraticRange(12)
          yield 'from', quadraticRange(8)
      

      嘿,这几乎就像 PEP!

      [学分:this answer 提供了类似的功能 - 但它很深(我认为这是错误的)并且不是作为装饰器的框架]

      【讨论】:

        【解决方案8】:
        class Communicator:
            def __init__(self, inflow):
                self.outflow = None
                self.inflow = inflow
        

        然后你做:

        c = Communicator(something)
        yield c
        response = c.outflow
        

        您可以简单地执行以下操作,而不是样板代码:

         for i in run():
             something = i.inflow
             # ...
             i.outflow = value_to_return_back
        

        代码很简单,不需要太多脑筋。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-08-22
          • 1970-01-01
          • 2010-09-12
          • 1970-01-01
          • 2011-07-29
          • 1970-01-01
          • 2014-07-29
          • 1970-01-01
          相关资源
          最近更新 更多