Como olhar para a frente um elemento (Peek) em um gerador de python?
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?
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