¿Cómo mirar hacia adelante un elemento (peek) en un generador de Python?

No puedo imaginar cómo mirar hacia adelante un elemento en un generador de Python. Tan pronto como lo veo se ha ido.

Esto es lo que quiero decir:

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! 

Aquí hay un ejemplo más real:

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

¿Alguien puede ayudarme a escribir un generador que pueda mirar un elemento hacia adelante?

    El API del generador de Python es una forma: no puede rechazar los elementos que ha leído. Pero puede crear un nuevo iterador utilizando el módulo itertools y anteponer el elemento:

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

    Para completar, el paquete more-itertools (que probablemente debería formar parte de la caja de herramientas de cualquier progtwigdor de Python) incluye un contenedor que se puede peekable que implementa este comportamiento. Como muestra el ejemplo de código en la documentación :

     >>> p = peekable(xrange(2)) >>> p.peek() 0 >>> p.next() 0 >>> p.peek() 1 >>> p.next() 1 

    El paquete es compatible con Python 2 y 3, aunque la documentación muestra la syntax de Python 2.

    Ok, dos años demasiado tarde, pero encontré esta pregunta y no encontré ninguna de las respuestas que me satisficieran. Llegó con este meta generador:

     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 

    resultados en:

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

    es decir, en cualquier momento durante la iteración tiene acceso al siguiente elemento de la lista.

    Puede usar itertools.tee para producir una copia liviana del generador. Luego, mirar una copia por adelantado no afectará a la segunda copia:

     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) 

    El generador de “elementos” no se ve afectado por el hecho de molestar a “peeker”. Tenga en cuenta que no debe usar el ‘seq’ original después de llamar ‘tee’ en él, eso romperá las cosas.

    FWIW, esta es la manera incorrecta de resolver este problema. Cualquier algoritmo que requiera que mire 1 elemento hacia adelante en un generador podría escribirse para usar el elemento generador actual y el elemento anterior. Entonces no tiene que alterar su uso de generadores y su código será mucho más simple. Vea mi otra respuesta a esta pregunta.

     >>> 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] 

    Solo por diversión, creé una implementación de una clase de búsqueda anticipada basada en la sugerencia de 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 

    Con esto, funcionará lo siguiente:

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

    Con esta implementación es una mala idea llamar a peek muchas veces seguidas …

    Mientras observaba el código fuente de CPython, encontré una mejor manera que es tanto más corta como más eficiente:

     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 

    El uso es el mismo que el anterior, pero no tendrá que pagar un precio aquí para usar peek muchas veces seguidas. Con unas pocas líneas más, también puede mirar hacia adelante más de un elemento en el iterador (hasta la RAM disponible).

    En lugar de usar ítems (i, i + 1), donde ‘i’ es el ítem actual e i + 1 es la versión ‘peek ahead’, debería estar usando (i-1, i), donde ‘i-1’ Es la versión anterior del generador.

    Ajustar su algoritmo de esta manera producirá algo que es idéntico a lo que tiene actualmente, además de la complejidad innecesaria de intentar “mirar hacia adelante”.

    Mirar hacia delante es un error, y no deberías hacerlo.

    Esto funcionará: amortigua un elemento y llama a una función con cada elemento y el siguiente elemento de la secuencia.

    Sus requisitos son confusos sobre lo que sucede al final de la secuencia. ¿Qué significa “mirar hacia adelante” cuando estás en la última?

     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 

    Una solución simple es usar una función como esta:

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

    Entonces puedes hacer:

     >>> it = iter(range(10)) >>> x, it = peek(it) >>> x 0 >>> next(it) 0 >>> next(it) 1 

    Si alguien está interesado, y corríjame si me equivoco, pero creo que es bastante fácil agregar alguna funcionalidad de retroceso a cualquier iterador.

     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 

    Aunque itertools.chain() es la herramienta natural para el trabajo aquí, tenga cuidado con los bucles como este:

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

    … Debido a que esto consumirá una cantidad de memoria que crece linealmente, y eventualmente se detendrá. (Este código esencialmente parece crear una lista enlazada, un nodo por llamada de cadena). Sé esto no porque inspeccioné las librerías, sino porque esto solo resultó en una mayor desaceleración de mi progtwig: deshacerme de la gen = itertools.chain([peek], gen) aceleró de nuevo. (Python 3.3)

    Fragmento de Python3 para @ jonathan-hartley respuesta:

     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) 

    Sería sencillo crear una clase que haga esto en __iter__ y produzca solo el elemento prev y ponga el elm en algún atributo.

    En la publicación de @David Z, la herramienta de seekable más seekable puede restablecer un iterador envuelto a una posición anterior.

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