Pergunta

Não consigo descobrir como olhar para a frente um elemento em um gerador Python. Assim que olho, ele se foi.

Aqui está o que quero dizer:

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!

Aqui está um exemplo mais real:

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

Alguém pode me ajudar a escrever um gerador que você pode procurar um elemento para a frente?

Foi útil?

Solução

A API do gerador Python é uma maneira: você não pode pressionar os elementos que leu. Mas você pode criar um novo iterador usando o Módulo ITERTOOLS e prenda o elemento:

import itertools

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

Outras dicas

Por uma questão de completude, o more-itertools pacote (que provavelmente deve fazer parte da caixa de ferramentas de qualquer programador Python) inclui um peekable invólucro que implementa esse comportamento. Como o exemplo de código em a documentação shows:

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

O pacote é compatível com o Python 2 e 3, mesmo que a documentação mostre a sintaxe do Python 2.

OK - dois anos tarde demais - mas me deparei com essa pergunta e não encontrei nenhuma das respostas para minha satisfação. Surgiu com este meta gerador:

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

resulta em:

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

ou seja, você tem a qualquer momento durante o acesso à iteração ao próximo item da lista.

Você pode usar o itertools.tee para produzir uma cópia leve do gerador. Em seguida, espreitar à frente em uma cópia não afetará a segunda cópia:

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)

O gerador de 'itens' não é afetado por você molestar o 'peeker'. Observe que você não deve usar o 'seq' original depois de ligar para 'tee', isso quebrará as coisas.

Fwiw, este é o errado maneira de resolver esse problema. Qualquer algoritmo que exija que você procure 1 item à frente em um gerador possa ser gravado para usar o item atual do gerador e o item anterior. Então você não precisa manguar o uso de geradores e seu código será muito mais simples. Veja minha outra resposta para esta pergunta.

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

Apenas por diversão, criei uma implementação de uma classe LookaHead com base na sugestão 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

Com isso, o seguinte funcionará:

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

Com esta implementação, é uma má idéia ligar para Peek muitas vezes seguidas ...

Enquanto olhava para o código -fonte do Cpython, acabei de encontrar uma maneira melhor que é mais curta e mais 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

O uso é o mesmo que acima, mas você não pagará um preço aqui para usar o Peek muitas vezes seguidas. Com mais algumas linhas, você também pode procurar mais de um item no iterador (à RAM disponível).

Em vez de usar itens (i, i+1), onde 'i' é o item atual e i+1 é a versão 'Peek Award', você deve usar (i-1, i), onde 'i-1' é a versão anterior do gerador.

Ajustar seu algoritmo dessa maneira produzirá algo idêntico ao que você tem atualmente, além da complexidade extra desnecessária de tentar 'espiar a frente'.

Espreitar a frente é um erro, e você não deve estar fazendo isso.

Isso funcionará - ele buffer um item e chama uma função com cada item e o próximo item na sequência.

Seus requisitos são obscuros sobre o que acontece no final da sequência. O que significa "olhar para a frente" quando você está no último?

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

Uma solução simples é usar uma função como esta:

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

Então você pode fazer:

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

Se alguém estiver interessado e, por favor, me corrija se estiver errado, mas acredito que é muito fácil adicionar alguma funcionalidade de empurrar a qualquer 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

Embora itertools.chain() é a ferramenta natural para o trabalho aqui, cuidado com loops como este:

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

... Porque isso consumirá uma quantidade de memória de crescimento linear e acabará por parar. (Este código parece essencialmente criar uma lista vinculada, um nó por chamada de cadeia ().) Eu sei disso não porque inspecionei o LIBS, mas porque isso apenas resultou em uma grande desaceleração do meu programa - livrar -se do gen = itertools.chain([peek], gen) A linha acelerou novamente. (Python 3.3)

Snippet Python3 para @Jonathan-Hartley responda:

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)

Seria simples criar uma classe que faça isso em __iter__ e produz apenas o prev item e colocar o elm em algum atributo.

Postagem do Wrt @David Z, o mais novo seekable A ferramenta pode redefinir um iterador embrulhado para uma posição 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 tem um olhadinha função.

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top