Pregunta

No puedo encontrar la manera de mirar hacia adelante un elemento en un generador de Python. Tan pronto como me veo que 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!

Este es un ejemplo más real:

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

Puede alguien ayudarme a escribir un generador que se puede ver un elemento hacia adelante?

¿Fue útil?

Solución

La API generador de Python es una manera: No se puede hacer retroceder a los elementos que ha leído. Sin embargo, puede crear un nuevo iterador utilizando el itertools módulo y anteponer el elemento:

import itertools

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

Otros consejos

Para mayor abundamiento, el more-itertools paquete (que probablemente debería ser parte de cualquier caja de herramientas del programador de Python) incluye un envoltorio peekable que implementa este comportamiento. Como el ejemplo de código en la documentación muestra:

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

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

Ok - dos años de retraso - pero me encontré con esta pregunta, y no encontró ninguna de las respuestas a mi satisfacción. Subió con este generador de meta:

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. usted tiene en cualquier momento durante la iteración de acceso al siguiente elemento de la lista.

Puede utilizar itertools.tee para producir una copia de peso ligero del generador. A continuación, asomándose por delante en una copia 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)

Las generador de 'artículos' que se ve afectado por abusar 'peeker'. Tenga en cuenta que no debe utilizar el original 'ss' después de llamar 'tee' en él, que romperá las cosas.

Fwiw, esta es la forma mal para resolver este problema. Cualquier algoritmo que requiere que se mire 1 punto por delante en un generador, alternativamente, se podría escribir a utilizar el elemento generador de corriente y el punto anterior. Entonces usted no tiene que destrozar el uso de generadores y su código será mucho más sencillo. Ver 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]

Sólo por diversión, he creado una implementación de una clase de búsqueda hacia delante sobre la base de la sugerencia 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, el siguiente trabajo:

>>> 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 aplicación es una mala idea llamar peek muchas veces en una fila ...

Mientras observa el código fuente CPython acabo de encontrar una mejor manera que es tanto más corto y 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 que no va a pagar un precio para utilizar peek muchas veces seguidas. Con unas cuantas más líneas también se puede mirar hacia adelante más de un artículo en el iterador (hasta RAM disponible).

En lugar de utilizar elementos (i, i + 1), donde 'i' es el elemento actual e i + 1 es la versión 'mirar adelante', usted debe utilizar (i-1, i), donde 'i -1' es la versión anterior del generador.

ajustar su algoritmo de esta manera va a producir algo que es idéntico a lo que actualmente tiene, aparte de la complejidad innecesaria adicional de tratar de 'mirar adelante'.

Echar un vistazo por delante es un error, y no debe hacerlo.

Esto funciona - se amortigua un elemento y llama a una función a cada elemento y el siguiente elemento de la secuencia

.

Sus necesidades no son claras en lo que sucede al final de la secuencia. ¿Qué significa "mirar hacia adelante" significa 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 utilizar una función como esta:

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

A continuación, puede hacer:

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

Si alguien está interesado, y por favor corríjanme si me equivoco, pero creo que es bastante fácil de añadir un poco de hacer retroceder a cualquier funcionalidad 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

A pesar de itertools.chain() es la herramienta natural para el trabajo aquí, tenga cuidado con los bucles de esta manera:

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

... Debido a que este va a consumir una cantidad linealmente creciente de la memoria, y con el tiempo se detendría. (Este código parece esencialmente para crear una lista enlazada, un nodo por cadena () llamada.) Sé que esto no porque inspeccioné las librerías sino porque esto sólo dio lugar a una importante desaceleración de mi programa - Deshacerse de la línea gen = itertools.chain([peek], gen) aceleró se de nuevo. (Python 3.3)

@ Jonathan-Hartley contesta:

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 hace esto en __iter__ y los rendimientos apenas el artículo prev y poner el elm en algún atributo.

El post de WRT @ David Z, el más reciente seekable herramienta 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

cytoolz tiene un asomarse función.

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top