如何在Python生成器中展望一个元素?

bod*_*ydo 68 python generator peek

我无法弄清楚如何在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!
Run Code Online (Sandbox Code Playgroud)

这是一个更实际的例子:

gen = element_generator()
if gen.next_value() == 'STOP':
  quit_application()
else:
  process(gen.next())
Run Code Online (Sandbox Code Playgroud)

任何人都可以帮我写一个发电机,你可以看一个元素前进吗?

Dav*_*d Z 69

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

>>> p = peekable(xrange(2))
>>> p.peek()
0
>>> p.next()
0
>>> p.peek()
1
>>> p.next()
1
Run Code Online (Sandbox Code Playgroud)

该软件包兼容Python 2和3,即使文档显示了Python 2语法.


Aar*_*lla 55

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

import itertools

gen = iter([1,2,3])
peek = gen.next()
print list(itertools.chain([peek], gen))
Run Code Online (Sandbox Code Playgroud)

  • 我提到这是在`more-itertools`包中实现的[`spy`](https://more-itertools.readthedocs.io/en/latest/api.html#more_itertools.spy).并不是说值得为这一个功能引入一个全新的包,但有些人可能会发现现有的实现很有用. (9认同)
  • 我已经多次使用过这个解决方案了,但我认为应该指出你基本上为你从迭代中得到的每个元素调用`itertools.chain .__ next__``n`次数(其中`n`是你偷看的次数).这适用于一两个偷看,但如果你需要窥视每个元素,这不是最好的解决方案:-) (5认同)
  • 您可以使用[`send`](http://docs.python.org/2/reference/expressions.html#generator)将先前产生的值推回到生成器中,因为它会产生下一个值. (4认同)
  • @dansalmo:是的,但您需要为此修改生成器代码。请参阅 Andrew Hare 的答案。 (3认同)
  • @mgilson 是的,这绝对应该带有警告。人们很可能会尝试在循环中执行此操作,查看每个元素,然后整个迭代需要二次时间。 (2认同)

plo*_*lof 25

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

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
Run Code Online (Sandbox Code Playgroud)

结果是:

0 3 False
3 6 False
6 9 False
9 12 False    
...
24 27 False
27 None False
Run Code Online (Sandbox Code Playgroud)

即,您在迭代期间的任何时刻都可以访问列表中的下一个项目.

  • @Jonathan在非平凡的例子中可能并不总是这样,例如当迭代器传递给函数时. (6认同)
  • 迟到总比不到好.:) (4认同)
  • 我觉得这样说有点意思,但我发现这个解决方案很可怕而且很容易出错。在任何时候,您都需要访问生成器中的两个项目:'i' 和 'i+1' 元素。为什么不编码您的算法以使用当前值和前一个值,而不是下一个值和当前值?它看起来完全相同,而且比这简单得多。 (4认同)
  • 有人应该指出,从python2.6开始,获取生成器的下一个值的首选方法是`next(generator)`而不是`generator.next()`.IIRC,`generator.next()`在python3.x中消失了.同样,为了获得最佳的向前兼容性,将`__next__ = next`添加到类的主体中,以便它继续在python3.x中工作.那说,很好的答案. (2认同)

Jon*_*ley 16

您可以使用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)
Run Code Online (Sandbox Code Playgroud)

'items'生成器不受你骚扰'peeker'的影响.请注意,在调用'tee'之后你不应该使用原来的'seq',否则会破坏它.

FWIW,这是解决这个问题的错误方法.任何要求您在生成器中查找前面的项目的算法也可以编写为使用当前生成器项目和前一项目.那么你不必破坏你对发生器的使用,你的代码会简单得多.请参阅我对此问题的其他答案.

  • "任何需要你在生成器中查看前面一项的算法都可以写成使用当前生成器项和前一项." 管理您对生成器的使用有时会导致更优雅和可读的代码,尤其是在需要前瞻的解析器中. (2认同)

Tho*_*hle 6

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

def peek(it):
    first = next(it)
    return first, itertools.chain([first], it)
Run Code Online (Sandbox Code Playgroud)

然后你可以这样做:

>>> it = iter(range(10))
>>> x, it = peek(it)
>>> x
0
>>> next(it)
0
>>> next(it)
1
Run Code Online (Sandbox Code Playgroud)


rob*_*ing 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]
Run Code Online (Sandbox Code Playgroud)


Blu*_*orn 5

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

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
Run Code Online (Sandbox Code Playgroud)

这样,以下将起作用:

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

通过这种实现,连续多次调用偷看是一个坏主意。

在查看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
Run Code Online (Sandbox Code Playgroud)

用法与上面相同,但您无需为此付出任何代价就可以连续使用peek多次。再增加几行,您还可以在迭代器中查找多个项(最多可用RAM)。


Ste*_*ann 5

一个迭代器,允许查看下一个元素以及更远的元素。它根据需要提前读取并记住 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]
Run Code Online (Sandbox Code Playgroud)

演示:

>>> 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]
>>>
Run Code Online (Sandbox Code Playgroud)