web-dev-qa-db-fra.com

Comment anticiper un élément dans un générateur Python?

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?

59
bodacydo

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))
47
Aaron Digulla

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.

60
David Z

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. 

23
plof

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.

15
Jonathan Hartley
>>> 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]
4
robert king

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).

4
Bluehorn

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.

3
Jonathan Hartley

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
2
S.Lott

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
1
nilo

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
1
Thomas Ahle

cytoolz a une fonction peek .

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
1
W.P. McNeill

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)

0
Jacob Eliosoff

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
0
pylang

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.

0
nitely