89

我不知道如何在 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())

谁能帮我写一个生成器,你可以向前看一个元素吗?

4

17 回答 17

91

为了完整起见,该more-itertools(可能应该是任何 Python 程序员工具箱的一部分)包含一个peekable实现此行为的包装器。如文档中的代码示例所示:

>>> p = peekable(['a', 'b'])
>>> p.peek()
'a'
>>> next(p)
'a'

但是,通常可以重写将使用此功能的代码,使其实际上不需要它。例如,问题中的实际代码示例可以这样编写:

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

(读者注意:我在写这篇文章时保留了问题中示例中的语法,即使它指的是 Python 的过时版本)

于 2014-12-30T01:59:05.340 回答
67

Python 生成器 API 是一种方式:您不能将已阅读的元素推回。但是您可以使用itertools 模块创建一个新的迭代器并添加元素:

import itertools

gen = iter([1,2,3])
peek = gen.next()
print list(itertools.chain([peek], gen))
于 2010-03-11T13:46:09.893 回答
24

好的 - 两年太晚了 - 但我遇到了这个问题,并没有找到任何令我满意的答案。想出了这个元生成器:

class Peekorator(object):

    def __init__(self, generator):
        self.empty = False
        self.peek = None
        self.generator = generator
        try:
            self.peek = self.generator.next()
        except StopIteration:
            self.empty = True

    def __iter__(self):
        return self

    def next(self):
        """
        Return the self.peek element, or raise StopIteration
        if empty
        """
        if self.empty:
            raise StopIteration()
        to_return = self.peek
        try:
            self.peek = self.generator.next()
        except StopIteration:
            self.peek = None
            self.empty = True
        return to_return

def simple_iterator():
    for x in range(10):
        yield x*3

pkr = Peekorator(simple_iterator())
for i in pkr:
    print i, pkr.peek, pkr.empty

结果是:

0 3 False
3 6 False
6 9 False
9 12 False    
...
24 27 False
27 None False

即,您在迭代过程中的任何时候都可以访问列表中的下一个项目。

于 2012-05-14T01:18:25.507 回答
17

您可以使用 itertools.tee 生成生成器的轻量级副本。然后往前看一个副本不会影响第二个副本:

import itertools

def process(seq):
    peeker, items = itertools.tee(seq)

    # initial peek ahead
    # so that peeker is one ahead of items
    if next(peeker) == 'STOP':
        return

    for item in items:

        # peek ahead
        if next(peeker) == "STOP":
            return

        # process items
        print(item)

'项目'生成器不受你骚扰'窥视者'的影响。请注意,在调用 'tee' 之后,您不应该使用原始的 'seq',这会破坏事情。

FWIW,这是解决此问题的错误方法。任何需要您在生成器中向前查看 1 个项目的算法都可以编写为使用当前生成器项目和前一个项目。然后你不必破坏你对生成器的使用,你的代码会简单得多。请参阅我对这个问题的其他答案。

于 2012-08-21T17:13:24.100 回答
7

一个迭代器,它允许查看下一个元素以及更远的位置。它根据需要提前读取并记住 a 中的值deque

from collections import deque

class PeekIterator:

    def __init__(self, iterable):
        self.iterator = iter(iterable)
        self.peeked = deque()

    def __iter__(self):
        return self

    def __next__(self):
        if self.peeked:
            return self.peeked.popleft()
        return next(self.iterator)

    def peek(self, ahead=0):
        while len(self.peeked) <= ahead:
            self.peeked.append(next(self.iterator))
        return self.peeked[ahead]

演示:

>>> it = PeekIterator(range(10))
>>> it.peek()
0
>>> it.peek(5)
5
>>> it.peek(13)
Traceback (most recent call last):
  File "<pyshell#68>", line 1, in <module>
    it.peek(13)
  File "[...]", line 15, in peek
    self.peeked.append(next(self.iterator))
StopIteration
>>> it.peek(2)
2
>>> next(it)
0
>>> it.peek(2)
3
>>> list(it)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>
于 2020-01-24T20:46:19.297 回答
6

只是为了好玩,我根据 Aaron 的建议创建了一个前瞻类的实现:

import itertools

class lookahead_chain(object):
    def __init__(self, it):
        self._it = iter(it)

    def __iter__(self):
        return self

    def next(self):
        return next(self._it)

    def peek(self, default=None, _chain=itertools.chain):
        it = self._it
        try:
            v = self._it.next()
            self._it = _chain((v,), it)
            return v
        except StopIteration:
            return default

lookahead = lookahead_chain

有了这个,以下将起作用:

>>> t = lookahead(xrange(8))
>>> list(itertools.islice(t, 3))
[0, 1, 2]
>>> t.peek()
3
>>> list(itertools.islice(t, 3))
[3, 4, 5]

使用此实现,连续多次调用 peek 是一个坏主意......

在查看 CPython 源代码时,我发现了一种更短、更高效的更好方法:

class lookahead_tee(object):
    def __init__(self, it):
        self._it, = itertools.tee(it, 1)

    def __iter__(self):
        return self._it

    def peek(self, default=None):
        try:
            return self._it.__copy__().next()
        except StopIteration:
            return default

lookahead = lookahead_tee

用法与上面相同,但您不会在这里为连续多次使用 peek 付出代价。通过多几行,您还可以在迭代器中查看多个项目(最多可用 RAM)。

于 2013-12-05T22:57:58.770 回答
6

一个简单的解决方案是使用这样的函数:

def peek(it):
    first = next(it)
    return first, itertools.chain([first], it)

然后你可以这样做:

>>> it = iter(range(10))
>>> x, it = peek(it)
>>> x
0
>>> next(it)
0
>>> next(it)
1
于 2017-01-30T10:08:31.533 回答
5
>>> gen = iter(range(10))
>>> peek = next(gen)
>>> peek
0
>>> gen = (value for g in ([peek], gen) for value in g)
>>> list(gen)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
于 2013-01-28T07:16:25.913 回答
3

这将起作用 - 它缓冲一个项目并使用序列中的每个项目和下一个项目调用一个函数。

您对序列结束时发生的事情的要求是模糊的。当你在最后一个时,“向前看”是什么意思?

def process_with_lookahead( iterable, aFunction ):
    prev= iterable.next()
    for item in iterable:
        aFunction( prev, item )
        prev= item
    aFunction( item, None )

def someLookaheadFunction( item, next_item ):
    print item, next_item
于 2010-03-11T13:53:19.010 回答
3

而不是使用项目 (i, i+1),其中“i”是当前项目,i+1 是“预览”版本,您应该使用 (i-1, i),其中“i-1”是生成器的先前版本。

以这种方式调整您的算法将产生与您当前拥有的相同的东西,除了尝试“窥视”的额外不必要的复杂性。

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

于 2012-08-21T17:08:39.807 回答
3

如果有人感兴趣,如果我错了,请纠正我,但我相信向任何迭代器添加一些推回功能非常容易。

class Back_pushable_iterator:
    """Class whose constructor takes an iterator as its only parameter, and
    returns an iterator that behaves in the same way, with added push back
    functionality.

    The idea is to be able to push back elements that need to be retrieved once
    more with the iterator semantics. This is particularly useful to implement
    LL(k) parsers that need k tokens of lookahead. Lookahead or push back is
    really a matter of perspective. The pushing back strategy allows a clean
    parser implementation based on recursive parser functions.

    The invoker of this class takes care of storing the elements that should be
    pushed back. A consequence of this is that any elements can be "pushed
    back", even elements that have never been retrieved from the iterator.
    The elements that are pushed back are then retrieved through the iterator
    interface in a LIFO-manner (as should logically be expected).

    This class works for any iterator but is especially meaningful for a
    generator iterator, which offers no obvious push back ability.

    In the LL(k) case mentioned above, the tokenizer can be implemented by a
    standard generator function (clean and simple), that is completed by this
    class for the needs of the actual parser.
    """
    def __init__(self, iterator):
        self.iterator = iterator
        self.pushed_back = []

    def __iter__(self):
        return self

    def __next__(self):
        if self.pushed_back:
            return self.pushed_back.pop()
        else:
            return next(self.iterator)

    def push_back(self, element):
        self.pushed_back.append(element)
it = Back_pushable_iterator(x for x in range(10))

x = next(it) # 0
print(x)
it.push_back(x)
x = next(it) # 0
print(x)
x = next(it) # 1
print(x)
x = next(it) # 2
y = next(it) # 3
print(x)
print(y)
it.push_back(y)
it.push_back(x)
x = next(it) # 2
y = next(it) # 3
print(x)
print(y)

for x in it:
    print(x) # 4-9
于 2017-10-23T21:03:43.903 回答
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放入某个属性的类会很简单。

于 2017-07-19T10:16:22.470 回答
1

wrt @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
于 2017-12-05T03:00:22.293 回答
1

cytoolz具有窥视功能。

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
于 2019-06-02T23:56:05.580 回答
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

这个概念在我编写解析器时非常有用,它需要逐行查看文件,如果该行似乎属于下一阶段的解析,我可以排队回到生成器,以便下一阶段的代码可以在不处理复杂状态的情况下正确解析它。

于 2021-09-10T08:45:07.313 回答
0

虽然itertools.chain()是这里工作的自然工具,但要注意这样的循环:

for elem in gen:
    ...
    peek = next(gen)
    gen = itertools.chain([peek], gen)

...因为这将消耗线性增长的内存量,并最终停止。(这段代码本质上似乎创建了一个链表,每个 chain() 调用一个节点。)我知道这不是因为我检查了库,而是因为这只是导致我的程序严重减速 - 摆脱gen = itertools.chain([peek], gen)线路加速它再次。(Python 3.3)

于 2015-02-05T20:37:58.460 回答
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

此方法通过避免多次复制迭代器来节省空间。由于它如何延迟生成元素,它也很快。最后,作为顶部的樱桃,您可以向前看任意数量的元素。

于 2021-02-03T01:17:51.537 回答