如何从生成器中选择一个项目?

345

我有一个类似下面的生成器函数:

def myfunct():
  ...
  yield result
通常调用此函数的方式是:
for r in myfunct():
  dostuff(r)

我的问题是,是否有一种方法可以随时从生成器中获取一个元素?例如,我想做这样的事情:

while True:
  ...
  if something:
      my_element = pick_just_one_element(myfunct())
      dostuff(my_element)
  ...
8个回答

459
使用该方法创建一个生成器。
g = myfunct()

每当你想要一个项目时,请使用:

next(g)
< p >如果生成器退出,它将引发StopIteration异常。您可以在必要时捕获此异常,或者使用next()default参数:

(或在Python 2.5或更低版本中使用g.next())。

next(g, default_value)

7
请注意,只有在提供了g中的最后一项之后,您尝试使用g.next()时才会引发StopIteration异常。 - Wilduck
35
next(gen, default)可以用于避免StopIteration异常。例如,对于一个生成字符串的生成器,使用next(g, None)将在迭代完成后产生一个字符串或者None。 - Attila
9
在Python 3000中,next()函数被重命名为__next__()方法。 - Jonathan Baldwin
32
@JonathanBaldwin: 你的评论有些误导性。在Python 3中,你会使用我答案中给出的第二种语法,即 next(g)。这将内部调用 g.__next__(),但你不必担心这个,就像你通常不关心 len(a) 内部调用 a.__len__() 一样。 - Sven Marnach
14
我应该表述得更清楚一些。在Python 3中,g.next()等同于g.__next__()。自Python 2.6起,就有了内置函数next(iterator),在所有新的Python代码中都应使用它,如果需要支持Python版本小于等于2.5,则可以轻松地回溯实现。 - Jonathan Baldwin
显示剩余4条评论

35

要从生成器中选择一个元素,请在 for 语句中使用 break,或者使用 list(itertools.islice(gen, 1))

根据你的示例,可以像这样操作:

while True:
  ...
  if something:
      for my_element in myfunct():
          dostuff(my_element)
          break
      else:
          do_generator_empty()

如果你只想随时获取“[一旦生成的]生成器中的一个元素”(我猜50%这是最初的意图,也是最常见的意图),那么:

gen = myfunct()
while True:
  ...
  if something:
      for my_element in gen:
          dostuff(my_element)
          break
      else:
          do_generator_empty()

这种方法可以避免明确使用 generator.next(),而且结束输入处理也不需要(晦涩的)StopIteration 异常处理或额外的默认值比较。

for 语句部分中的 else: 只在想要在生成器结束时执行特殊操作时才需要。

next() / .next() 注意事项:

在 Python3 中,.next() 方法被重命名为 .__next__(),这是有充分理由的(PEP 3114)。在 Python 2.6 之前,内置函数 next() 并不存在。甚至曾经有讨论将 next() 移动到 operator 模块中(这是明智的),因为它很少使用并且增加内置名称的问题值得商榷。

在没有默认值的情况下使用 next() 仍然是非常低级的做法 - 在正常应用程序代码中突然抛出晦涩难懂的 StopIteration 异常。而使用带有默认位置标记的 next() - 最好是直接在 builtins 中唯一的选项 - 是有限制的并且常常导致奇怪的非Pythonic逻辑/可读性。

底线:使用 next() 应该非常罕见 - 就像使用 operator 模块的函数一样。在应用程序级别上,使用 for x in iteratorislicelist(iterator) 和其他接受迭代器的函数是使用迭代器的自然方式,并且几乎总是可行的。 next() 是低级的,额外的概念,不明显的 - 就像这个主题的问题一样。而在 for 中使用 break 则是常规做法。


15
为了获取列表结果的第一个元素,这太费力了。通常情况下,我不需要它是延迟计算的,但在Python3中我没有选择。是否有类似于mySeq.head的东西? - WestCoastProjects
1
在我看来,“for ... break”模式很糟糕,因为直到遇到结尾处的“break”之前,我们无法清楚地了解“for”的意图(问题在于必须将其放置在结尾处),因此至少应该在“for”处写一个注释;我更喜欢看到“with x in <generator>:”语法。 - ToxiCore
1
根据PEP 3114的规定,next在方法定义中被重命名,但内置方法没有改变。换句话说,在Python 3中,next(x)是可以的,但x.next()不起作用。 - Stevoisiak

18

生成器是一个产生迭代器的函数。因此,一旦您拥有迭代器实例,请使用next()从迭代器中获取下一个项。 例如,使用next()函数获取第一个项,然后使用for in来处理剩余的项:

# create new instance of iterator by calling a generator function
items = generator_function()

# fetch and print first item
first = next(items)
print('first item:', first)

# process remaining items:
for item in items:
    print('next item:', item)

9
您可以使用解构来选择特定的项,例如:
>>> first, *middle, last = range(10)
>>> first
0
>>> middle
[1, 2, 3, 4, 5, 6, 7, 8]
>>> last
9

请注意,这将耗尽您的生成器,因此虽然易于阅读,但效率不如像next()这样的方法,并且对于无限生成器来说是破坏性的。
>>> first, *rest = itertools.count()


3
我不认为有一种方便的方法来从生成器中检索任意值。生成器提供了一个next()方法来遍历自身,但是为了节省内存,完整的序列不会立即产生。这就是生成器和列表之间的功能区别。

0
generator = myfunct()
while True:
   my_element = generator.next()

确保在取出最后一个元素后捕获抛出的异常


不适用于Python 3,请参考kxr的优秀答案(https://dev59.com/5m445IYBdhLWcg3wq8Pp#35370041)。 - clacke
2
只需将 Python 3 中的 "generator.next()" 替换为 "next(generator)"。 - Roy

0

对于那些正在浏览这些答案以获取Python3的完整工作示例的人...好了,这里给你:

def numgen():
    x = 1000
    while True:
        x += 1
        yield x

nums = numgen() # because it must be the _same_ generator

for n in range(3):
    numnext = next(nums)
    print(numnext)

这将输出:

1001
1002
1003

-4

我相信唯一的方法是从迭代器中获取一个列表,然后从该列表中获取您想要的元素。

l = list(myfunct())
l[4]

Sven的回答可能更好,但我会把这个留在这里,以防它更符合您的需求。 - keegan3d
28
在进行此操作之前,请确保您拥有一个有限的生成器。 - Seth
7
抱歉,这个问题的复杂度应该是 O(1),但迭代器的长度让它变得复杂了。 - yo'
1
浪费太多的内存和进程来从生成器中提取! 此外,正如@Seth之前提到的那样,生成器不能保证何时停止生成。 - pylover
这显然 不是 唯一的方法(如果 myfunct() 生成了大量值,这也不是最佳方法),因为您可以使用内置函数 next 来获取下一个生成的值。 - HelloGoodbye

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