结构化编程与Python生成器?

6 投票
8 回答
1410 浏览
提问于 2025-04-15 16:04

更新:我一直想要的其实是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:
    # ...

有没有人有解决这个问题的办法?我有一个初步的尝试,但我想知道别人有什么想法。最终,任何解决方案都必须处理一些例子,其中主生成器根据发送到生成器的数据结果执行复杂的逻辑,并可能会对子生成器进行大量调用:我的使用场景是生成器用于实现长时间运行的复杂状态机。

8 个回答

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 的待办事项列表。希望这个答案很快就能变得实际可用。

阅读 Guido 在 comp.python.devel 上的评论

6

你想把几个迭代器连在一起:

from itertools import chain

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

不过,我想让我的可重用性标准更严格一点:如果我需要在重复生成的周围加一个控制结构呢?

itertools 通常在这方面也能提供帮助——你需要提供具体的例子,说明你认为它不适用的地方。

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

itertools.chain.from_iterable

或者我的子生成器可能非常耗费资源,我只想在需要的时候才启动它们。

无论是 chain 还是 chain_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 函数;等等。

如果你能提供一个不容易用这些技术解决的例子,我建议你在一个单独的问题中提出(特别针对类似协程的生成器),因为关于这个主题已经有很多材料,而这些材料与你其他更复杂/更精细的例子关系不大。

撰写回答