如何在Python生成器中向前查看一个元素(peek)?

106

我不知道如何在Python生成器中预测下一个元素。一旦我看到它,它就消失了。

这就是我的意思:

gen = iter([1,2,3])
next_value = gen.next()  # okay, I looked forward and see that next_value = 1
# but now:
list(gen)  # is [2, 3]  -- the first value is gone!

这里是一个更真实的例子:

gen = element_generator()
if gen.next_value() == 'STOP':
  quit_application()
else:
  process(gen.next())

有人可以帮我编写一个能够向前查看一个元素的生成器吗?


另请参阅: 如何在Python中重置生成器对象


1
你能详细描述一下你想做什么吗?可以提供代码示例吗? - Tim Pietzcker
我仍然不明白那有什么好处。如果生成器已经“耗尽”,则要避免调用gen.next()。这就是异常StopIteration的作用。在元素“STOP”之后生成器中有什么?为什么生成器在此时不会被简单地耗尽?或者,如果不可能,为什么不让生成器在当前元素为“Stop”时raise StopIteration - Tim Pietzcker
我开始认为正确的方法是修改算法,使用生成器的“当前”和“前一个”值,而不是尝试使用“下一个”和“当前”值。我相信没有任何算法不能以这种方式重构,这比提供的任何解决方案都要简单(包括我的两个解决方案)。 - Jonathan Hartley
我认为如果Python强制你改变一个完全合理的算法才能让它工作,那就是它的限制。但是,嘿,这是语言哲学的一部分。 - Steven Lu
@StevenLu 这太疯狂了!问题不在于语言,而在于OP有一个不可寻址流的两个不同读取器的糟糕想法,然后通过尝试使它们不互相干扰来弥补这种糟糕的设计。从生成器中只使用一个读取器,而不是两个,所有这些问题都会消失,不需要任何代码。 - Jonathan Hartley
显示剩余6条评论
18个回答

3

不要使用(i, i+1)这种方式,其中'i'是当前项,i+1是“向前查看”的版本,而应该使用(i-1, i)这种方式,其中'i-1'是生成器中的上一个版本。

通过这种方式调整你的算法将产生与当前算法相同的结果,除了额外的不必要的复杂性——试图“向前查看”。

向前查看是一个错误,你不应该这样做。


1
在你决定是否需要一个生成器中的项之前,你需要先取出它。比如一个函数从生成器中取出一个项,在检查后发现不需要它。如果你能将该项推回生成器中,那么下一个使用该生成器的用户就看不到该项了。而通过Peeking操作可以避免这种情况,无需将项推回生成器中。 - Isaac Turner
@IsaacTurner 不需要这样做。例如,您可以有两个嵌套的生成器。内部生成器接收一个项目,决定不想对其进行任何操作,然后无论如何都会产生它。外部生成器仍然可以看到序列中的所有内容。有等效的非嵌套生成器方法来完成相同的事情。只需记住变量中的“上一个项目”,您就可以执行此问题要求的任何操作。比尝试将事物推回要简单得多。 - Jonathan Hartley

1
尽管 itertools.chain() 是此处自然的工具,但要注意像这样的循环:
for elem in gen:
    ...
    peek = next(gen)
    gen = itertools.chain([peek], gen)

因为这将消耗线性增长的内存,并最终停止运行。(此代码似乎创建一个链表,每个chain()调用一个节点。)我知道这不是因为我检查了库,而是因为这导致我的程序显着减速 - 去掉gen = itertools.chain([peek], gen)这一行后程序加速了。(Python 3.3)

1
关于 @David Z 的帖子,新的 seekable 工具可以将包装后的迭代器重置到之前的位置。
>>> s = mit.seekable(range(3))
>>> s.next()
# 0

>>> s.seek(0)                                              # reset iterator
>>> s.next()
# 0

>>> s.next()
# 1

>>> s.seek(1)
>>> s.next()
# 1

>>> next(s)
# 2

1
在我的情况下,我需要一个生成器,可以通过队列将我刚刚通过next()调用获取的数据返回给生成器。
我处理这个问题的方法是创建一个队列。在生成器的实现中,我首先会检查队列:如果队列不为空,则“yield”将返回队列中的值,否则以正常方式返回值。
import queue


def gen1(n, q):
    i = 0
    while True:
        if not q.empty():
            yield q.get()
        else:
            yield i
            i = i + 1
            if i >= n:
                if not q.empty():
                    yield q.get()
                break


q = queue.Queue()

f = gen1(2, q)

i = next(f)
print(i)
i = next(f)
print(i)
q.put(i) # put back the value I have just got for following 'next' call
i = next(f)
print(i)

运行中

python3 gen_test.py

0
1
1

这个概念在我编写解析器时非常有用,因为解析器需要逐行查看文件,如果某一行似乎属于下一个解析阶段,我只需将其排队返回给生成器,以便代码的下一个阶段能够正确解析它,而无需处理复杂状态。

1

cytoolz有一个peek函数。

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]

1

以下是与 @jonathan-hartley 回答相关的 Python3 代码片段:

def peek(iterator, eoi=None):
    iterator = iter(iterator)

    try:
        prev = next(iterator)
    except StopIteration:
        return iterator

    for elm in iterator:
        yield prev, elm
        prev = elm

    yield prev, eoi


for curr, nxt in peek(range(10)):
    print((curr, nxt))

# (0, 1)
# (1, 2)
# (2, 3)
# (3, 4)
# (4, 5)
# (5, 6)
# (6, 7)
# (7, 8)
# (8, 9)
# (9, None)

创建一个类来实现这个功能非常简单,只需要在__iter__方法中返回prev元素并将elm存储在某个属性中即可。请保留HTML标签。

这种方法的问题在于您提前一步获取下一个元素,这可能是不可取的。例如,如果获取元素很慢或具有副作用。 - Maëlan
@Maëlan 这将是一个问题,无论如何都需要获取元素的值,才能找出它是什么。 - Karl Knechtel
@KarlKnechtel 在某些情况下,您可能会根据最后获取的元素的值或其他上下文来决定停止迭代。您不需要也不想再获取另一个元素。 - Maëlan
在这种情况下,编写相应的逻辑,以便它发生在前一个循环迭代中?尽管在某些特殊情况下,可能需要特殊处理以检查循环开始之前的第一个元素... - Karl Knechtel

0

一种通过“窥视”生成器中下一个元素的算法,等效于一种通过记住前一个元素来工作的算法,将该元素视为要操作的元素,并将“当前”元素视为仅仅是“查看”的元素。

无论哪种方式,真正发生的事情是算法从生成器中考虑重叠对itertools.tee配方可以很好地工作 - 而且不难看出它实质上是Jonathan Hartley方法的重构版本:

from itertools import tee
# From https://docs.python.org/3/library/itertools.html#itertools.pairwise
# In 3.10 and up, this is directly supplied by the `itertools` module.
def pairwise(iterable):
    # pairwise('ABCDEFG') --> AB BC CD DE EF FG
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)

def process(seq):
    for to_process, lookahead in pairwise(seq):
        # peek ahead
        if lookahead == "STOP":
            return
        # process items
        print(to_process)

对等的反证可能就是在类型检查中强制不产生任何副作用。 - undefined

0

对于那些拥抱节俭和一行代码的人,我向你们介绍一个允许在可迭代对象中向前查看的一行代码(仅适用于Python 3.8及以上版本):

>>> import itertools as it
>>> peek = lambda iterable, n=1: it.islice(zip(it.chain((t := it.tee(iterable))[0], [None] * n), it.chain([None] * n, t[1])), n, None)
>>> for lookahead, element in peek(range(10)):
...     print(lookahead, element)
1 0
2 1
3 2
4 3
5 4
6 5
7 6
8 7
9 8
None 9
>>> for lookahead, element in peek(range(10), 2):
...     print(lookahead, element)
2 0
3 1
4 2
5 3
6 4
7 5
8 6
9 7
None 8
None 9

这种方法通过避免多次复制迭代器而具有节省空间的优点。由于它懒惰地生成元素,因此也非常快速。最后,作为额外的福利,您可以向前查看任意数量的元素。


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