Domanda

Non riesco a capire come guardare avanti un elemento in un generatore di Python. Appena mi guardo non c'è più.

Ecco quello che voglio dire:

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!

Ecco un altro esempio reale:

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

Qualcuno può aiutarmi a scrivere un generatore che si può guardare un elemento in avanti?

È stato utile?

Soluzione

L'API generatore di Python è un modo: non è possibile spingere indietro gli elementi che hai letto. Ma è possibile creare un nuovo iteratore utilizzando il itertools modulo e anteporre l'elemento:

import itertools

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

Altri suggerimenti

Per completezza, il more-itertools pacchetto (che dovrebbe probabilmente essere parte di qualsiasi cassetta degli attrezzi del programmatore Python) include un wrapper peekable che implementa questo comportamento. Come l'esempio di codice in la documentazione mostra:

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

Il pacchetto è compatibile sia con Python 2 e 3, anche se la documentazione mostra sintassi Python 2.

Ok - due anni di ritardo - ma mi sono imbattuto in questa domanda, e non ha trovato alcuna delle risposte per la mia soddisfazione. Si avvicinò con questa meta generatore:

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

Risultati in:

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

vale a dire. avete in qualsiasi momento durante l'iterazione l'accesso alla voce successiva nella lista.

È possibile utilizzare itertools.tee per produrre una copia leggera del generatore. Poi capolino davanti ad una copia non influenzerà la seconda 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)

generatore Le 'voci' non è influenzato da voi molestare 'Peeker'. Si noti che non si dovrebbe usare l'originale 'ss' dopo aver chiamato 'Tee' su di esso, che si romperà le cose.

FWIW, questo è il sbagliato modo per risolvere questo problema. Qualsiasi algoritmo che richiede di guardare avanti 1 articolo in un generatore potrebbe in alternativa essere scritto per utilizzare la voce generatore di corrente e la voce precedente. Poi non c'è bisogno di storpiare l'utilizzo di generatori e il codice sarà molto più semplice. Vedere la mia altra risposta a questa domanda.

>>> 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 per divertimento, ho creato un'implementazione di una classe lookahead basato sul suggerimento di 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 questo, il seguente funziona:

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

Con questa implementazione è una cattiva idea quella di chiamare in PEEK molte volte di fila ...

Mentre si guarda il codice sorgente CPython Ho appena trovato un modo migliore, che è allo stesso tempo più breve e più efficiente:

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

L'uso è lo stesso come sopra, ma non pagherà un prezzo qui per utilizzare PEEK molte volte di fila. Con poche righe più si può anche guardare avanti più di una voce nel iteratore (fino a RAM disponibile).

Invece di usare elementi (i, i + 1), dove 'i' è l'elemento corrente e i + 1 è la versione 'sbirciare avanti', si dovrebbe utilizzare (i-1, i), dove 'i -1' è la versione precedente dal generatore.

Ottimizzare il vostro algoritmo in questo modo produrrà qualcosa che è identico a quello che attualmente si dispone, a parte la complessità in più inutile di cercare di 'sbirciare avanti'.

Sbirciare avanti è un errore, e non si dovrebbe fare di esso.

Ciò funzionerà - si tampona un elemento e chiama una funzione con ogni elemento e l'elemento successivo nella sequenza

.

Le vostre esigenze sono oscuri su ciò che accade alla fine della sequenza. Che cosa significa "guardare avanti" significa quando sei al l'ultimo?

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 soluzione semplice è quella di utilizzare una funzione come questa:

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

Poi si può fare:

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

Se qualcuno è interessato, e per favore mi corregga se sbaglio, ma credo che sia abbastanza facile aggiungere alcune funzionalità push back a qualsiasi iteratore.

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

Anche se itertools.chain() è lo strumento naturale per il lavoro qui, fate attenzione di loop come questo:

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

... Perché questo consuma una quantità linearmente crescente di memoria, e alla fine fermerebbe. (Questo codice sembra essenzialmente per creare una lista collegata, un nodo per catena () chiamata.) So che questo non perché ho controllato le librerie, ma perché questo solo ha provocato un forte rallentamento del mio programma - Come liberarsi di linea gen = itertools.chain([peek], gen) accelerato esso di nuovo. (Python 3.3)

@ jonathan-Hartley rispondere:

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)

Sarebbe semplice per creare una classe che fa questo su __iter__ e rese solo la voce prev e mettere il elm in qualche attributo.

Il post di WRT @ Davide Z, il più recente seekable strumento può ripristinare un iteratore avvolta in una posizione nota.

>>> 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 ha un sbirciare funzione.

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top