【问题标题】:How to look ahead one element (peek) in a Python generator?如何在 Python 生成器中向前看一个元素(窥视)?
【发布时间】:2020-02-06 08:10:55
【问题描述】:

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

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

【问题讨论】:

  • 你能更详细地描述你想要做什么吗?也许是代码示例?
  • 如果你有一个现有的列表,你还需要什么?另外,您似乎将第一个值保存为next_value,不是吗?
  • SilentGhost,这是一个说明gone 含义的示例。我没有列表,也没有 next_value。这只是一个示例,说明元素从生成器中消失意味着什么。
  • @bodacydo:我还是不明白。 怎么那么它消失了?为什么您无法访问该值?
  • 蒂姆,用一个更好的例子更新了这个问题。

标签: python generator peek


【解决方案1】:

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

>>> 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 版本)

【讨论】:

    【解决方案2】:

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

    import itertools
    
    gen = iter([1,2,3])
    peek = gen.next()
    print list(itertools.chain([peek], gen))
    

    【讨论】:

    • 您可以使用send 将先前产生的值推回生成器,因为它产生下一个值。
    • @dansalmo:是的,但是您需要为此修改生成器代码。请参阅 Andrew Hare 的答案。
    • 我已经多次使用此解决方案,但我认为可能应该指出的是,对于从可迭代对象中取出的每个元素,您基本上都调用了itertools.chain.__next__n 次(其中@ 987654328@ 是你偷看的次数)。这适用于一两次偷看,但如果您需要偷看每个元素,这不是最好的解决方案:-)
    • 我会提到这是在more-itertools 包中实现的spy。并不是说值得为这一项功能引入一个全新的包,但有些人可能会发现现有的实现很有用。
    • @mgilson 是的,这绝对应该带有警告。人们很可能会尝试循环执行此操作,查看每个元素,然后整个迭代需要二次时间。
    【解决方案3】:

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

    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
    

    即在迭代过程中,您可以随时访问列表中的下一项。

    【讨论】:

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

    您可以使用 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 个项目的算法都可以编写为使用当前生成器项目和前一个项目。然后你不必破坏你对生成器的使用,你的代码会简单得多。请参阅我对此问题的其他回答。

    【讨论】:

    • “任何需要您在生成器中向前查看 1 个项目的算法都可以编写为使用当前生成器项目和前一个项目。”修改生成器的使用有时会导致代码更优雅、更易读,尤其是在需要前瞻的解析器中。
    • 嘿,Rufflewind。我理解解析需要前瞻的观点,但我不明白为什么你不能通过简单地将前一个项目存储在生成器中并使用生成器中的最新项目作为前瞻来实现这一点。然后,您将获得两全其美:未损坏的生成器和简单的解析器。
    • 嗯,这就是为什么您将生成器包装在自定义类中以自动执行此操作。
    • 嘿,Ruffelwind。我不再确定我是否理解您的主张。很抱歉失去了剧情。
    • FWIW,代码现已修复,@Eric\ May 关于整个迭代器被缓冲的评论不再正确。
    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

      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)。

      【讨论】:

        【解决方案7】:

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

        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
        

        【讨论】:

          【解决方案8】:
          >>> 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]
          

          【讨论】:

          • 您介意解释一下这里发生的事情吗
          • 我们偷看 gen。然后我们创建一个可迭代的 [peek] 并将其与 gen 的其余部分结合起来以创建一个新的 gen。这是通过迭代两个生成器的展平来完成的,这两个生成器结合起来给出原始的。见扁平化:stackoverflow.com/questions/952914/…
          • 这是一样的,但比 itertools.chain 解决方案更明确。
          【解决方案9】:

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

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

          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
          

          【讨论】:

            【解决方案10】:

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

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

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

            【讨论】:

            • 您需要先从生成器中取出物品,然后才能知道是否需要。假设一个函数从生成器中获取一个项目,经过检查后决定它不想要它。除非您可以将其推回,否则生成器的下一个用户将看不到该项目。偷看删除需要推回项目。
            • @IsaacTurner 不,您不需要这样做。例如,您可以有两个嵌套的生成器。内部的人拿了一个项目,决定它不想对它做任何事情,然后无论如何都让出它。外部的仍然可以看到序列中的所有内容。有相当的、非常简单的方法可以在没有嵌套生成器的情况下做同样的事情。只需记住变量中的“上一个项目”,您就可以执行此问题要求的任何操作。比试图推回事情简单得多。
            【解决方案11】:

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

            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
            

            【讨论】:

              【解决方案12】:

              @jonathan-hartley 的 Python3 sn-p 答案:

              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 放入某个属性的类会很简单。

              【讨论】:

                【解决方案13】:

                w.r.t @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
                

                【讨论】:

                  【解决方案14】:

                  cytoolz 有一个peek 函数。

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

                  【讨论】:

                    【解决方案15】:

                    就我而言,我需要一个生成器,我可以在其中排队以生成我刚刚通过 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
                    

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

                    【讨论】:

                      【解决方案16】:

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

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

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

                      【讨论】:

                        【解决方案17】:

                        对于那些崇尚节俭和单行的人,我向你们展示了一个单行,它允许人们在可迭代中向前看(这只适用于 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
                        

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

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 1970-01-01
                          • 2011-10-06
                          • 1970-01-01
                          • 2020-01-13
                          • 1970-01-01
                          • 2021-04-14
                          • 2015-12-20
                          相关资源
                          最近更新 更多