Je n'arrive pas à comprendre comment anticiper un élément dans un générateur Python. Dès que je regarde il est parti.
Voici ce que je veux 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!
Voici un exemple plus réel:
gen = element_generator()
if gen.next_value() == 'STOP':
quit_application()
else:
process(gen.next())
Quelqu'un peut-il m'aider à écrire un générateur sur lequel vous pouvez regarder d'un élément?
L'API du générateur Python est un moyen: vous ne pouvez pas repousser les éléments que vous avez lus. Mais vous pouvez créer un nouvel itérateur à l’aide du module itertools et ajouter l’élément:
import itertools
gen = iter([1,2,3])
peek = gen.next()
print list(itertools.chain([peek], gen))
Par souci d'exhaustivité, le package more-itertools
(qui devrait probablement faire partie de la boîte à outils de tout développeur Python) inclut un wrapper peekable
qui implémente ce comportement. Comme l'exemple de code dans la documentation montre:
>>> p = peekable(xrange(2))
>>> p.peek()
0
>>> p.next()
0
>>> p.peek()
1
>>> p.next()
1
Le package est compatible avec Python 2 et 3, même si la documentation présente la syntaxe Python 2.
Ok - deux ans trop tard - mais je suis tombé sur cette question et je n’ai trouvé aucune réponse satisfaisante. Entré avec ce générateur de méta:
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
résulte en:
0 3 False
3 6 False
6 9 False
9 12 False
...
24 27 False
27 None False
c'est-à-dire que vous avez à tout moment pendant l'itération l'accès à l'élément suivant de la liste.
Vous pouvez utiliser itertools.tee pour produire une copie allégée du générateur. Puis regarder en avant une copie n’affectera pas la seconde copie:
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)
Le générateur "Objets" n'est pas affecté par votre agression "Peeker". Notez que vous ne devriez pas utiliser l'original 'seq' après avoir appelé 'tee' dessus, cela casserait des choses.
FWIW, c’est le mauvais moyen de résoudre ce problème. Tout algorithme nécessitant que vous regardiez un élément à l’avant dans un générateur peut également être écrit pour utiliser l’élément actuel du générateur et l’élément précédent. Dans ce cas, vous ne devez pas modifier votre utilisation des générateurs et votre code sera beaucoup plus simple. Voir mon autre réponse à cette question.
>>> 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]
Juste pour le plaisir, j'ai créé une implémentation d'une classe lookahead basée sur la suggestion 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
Avec ceci, ce qui suit fonctionnera:
>>> t = lookahead(xrange(8))
>>> list(itertools.islice(t, 3))
[0, 1, 2]
>>> t.peek()
3
>>> list(itertools.islice(t, 3))
[3, 4, 5]
Avec cette implémentation, c'est une mauvaise idée d'appeler plusieurs fois de suite un coup d'oeil ...
En regardant le code source de CPython, je viens de trouver un meilleur moyen, à la fois plus court et plus efficace:
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'utilisation est la même que ci-dessus, mais vous ne paierez pas le prix que vous utilisez plusieurs fois de suite. Avec quelques lignes supplémentaires, vous pouvez également rechercher plus d’un élément dans l’itérateur (jusqu’à la RAM disponible).
Au lieu d'utiliser les éléments (i, i + 1), où «i» est l'élément actuel et i + 1, la version «anticipation», vous devriez utiliser (i-1, i), où «i-1» est la version précédente du générateur.
En ajustant votre algorithme de cette manière, vous obtiendrez un résultat identique à celui que vous avez actuellement, mis à part la complexité inutile d’essayer de «regarder en avant».
Regarder en avant est une erreur, et vous ne devriez pas le faire.
Cela fonctionnera - il tamponne un élément et appelle une fonction avec chaque élément et l'élément suivant de la séquence.
Vos exigences sont obscures sur ce qui se passe à la fin de la séquence. Qu'est-ce que "regarder devant" signifie quand vous êtes au dernier?
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
Si quelqu'un est intéressé, corrigez-moi si je me trompe, mais je pense qu'il est assez facile d'ajouter des fonctionnalités Push back à tout itérateur.
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
Une solution simple consiste à utiliser une fonction comme celle-ci:
def peek(it):
first = next(it)
return first, itertools.chain([first], it)
Ensuite, vous pouvez faire:
>>> it = iter(range(10))
>>> x, it = peek(it)
>>> x
0
>>> next(it)
0
>>> next(it)
1
Bien que itertools.chain()
soit l'outil naturel du travail ici, méfiez-vous des boucles comme celle-ci:
for elem in gen:
...
peek = next(gen)
gen = itertools.chain([peek], gen)
... Parce que cela consommera une quantité de mémoire de plus en plus grande, et finira par s'arrêter. (Ce code semble essentiellement créer une liste chaînée, un nœud par appel chain ().) Je le sais pas parce que j'ai inspecté les bibliothèques, mais parce que cela a entraîné un ralentissement important de mon programme: suppression de la ligne gen = itertools.chain([peek], gen)
de nouveau. (Python 3.3)
le message de w.r.t @David Z, le plus récent seekable
tool peut réinitialiser un itérateur encapsulé à une position antérieure.
>>> 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
Extrait Python3 pour @ jonathan-hartley answer:
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)
Il serait simple de créer une classe qui effectue cela sur __iter__
et ne génère que l'élément prev
et place la Elm
dans un attribut.