6

更新:我一直真正想要的是greenlets


注意:当人们回答并迫使我“提高赌注”时,这个问题发生了一些变化,因为我的琐碎示例进行了琐碎的简化;根据亚历克斯的建议,我不会在这里继续改变它,而是在我头脑更清楚时提出这个问题。


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:
    # ...

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

4

8 回答 8

11

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

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

例如,我可能想用不同的参数永远调用一个子生成器。

itertools.chain.from_iterable.

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

两者都chain这样chain_from_iterable做 - 直到需要它的第一项之前,没有子迭代器被“启动”。

或者(这是一个真正的愿望)我可能想根据我的控制器使用 send() 传递给我的内容来改变我接下来要做的事情。

一个具体的例子将不胜感激。无论如何,最坏的情况是,您将for x in blargh: yield x在暂停的 Pep3080 允许您编码的地方进行编码yield from blargh——大约 4 个额外的字符(不是悲剧;-)。

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

例如,假设我们经常发现自己在做这样的事情:首先产生某个值;然后,重复地,如果我们被发送'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 内置函数有一个稍微不同的组合器;等等。

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

于 2009-11-16T23:34:58.377 回答
6

您想将多个迭代器链接在一起:

from itertools import chain

def sillyGenerator(a,b,c):
    return chain(quadraticRange(a),quadraticRange(b),quadraticRange(c))
于 2009-11-16T22:50:30.043 回答
6

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

from __future__ import PEP0380

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

潜在的实用参考:委托给子生成器的语法

不幸的是,这不切实际:Python 语言暂停

2011 年 2 月更新:

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

阅读Guido 关于 comp.python.devel 的评论

于 2009-11-16T23:10:58.577 回答
3
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])

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

于 2009-11-16T22:50:25.217 回答
3

有一个 Python Enhancement Proposal 提供了“委托代”的yield from声明。你的例子可以写成:

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)

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

于 2009-11-16T23:09:37.283 回答
2

对于任意数量的调用quadraticRange

from itertools import chain

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

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

于 2009-11-16T23:02:13.820 回答
1

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

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

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!

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

于 2009-12-31T14:32:51.240 回答
0
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

这是足够简单的代码,无需太多大脑即可工作。

于 2011-01-03T09:11:37.330 回答