Comment regarder en avant un élément (coup d'oeil) dans un générateur de Python?

StackOverflow https://stackoverflow.com/questions/2425270

  •  19-09-2019
  •  | 
  •  

Question

Je ne peux pas comprendre comment regarder vers l'avenir un élément dans un générateur de 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 me aider à écrire un générateur que vous pouvez regarder un élément en avant?

Était-ce utile?

La solution

L'API générateur de Python est une façon: Vous ne pouvez pas repousser les éléments que vous avez lu. Mais vous pouvez créer une nouvelle iterator en utilisant le itertools Module et préfixer l'élément:

import itertools

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

Autres conseils

Par souci d'exhaustivité, le (qui devrait probablement package more-itertools faire partie de tout la boîte à outils du programmeur Python) comprend une enveloppe peekable qui implémente ce comportement. Comme l'exemple de code la documentation indique:

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

Le paquet est compatible avec Python 2 et 3, même si la documentation montre la syntaxe Python 2.

Ok - deux ans trop tard - mais je suis tombé sur cette question, et n'a trouvé aucune des réponses à ma satisfaction. Entré avec ce générateur meta:

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ésultats dans:

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

i.e.. vous avez à tout moment lors de l'itération l'accès à l'élément suivant dans la liste.

Vous pouvez utiliser itertools.tee pour produire une copie légère du générateur. jeter un oeil à l'avance, puis une copie n'affectera pas la deuxième 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 'articles de n'est pas affectée par vous molester « Peeker ». Notez que vous ne devez pas utiliser l'original « suivants » après avoir appelé « tee » sur elle, qui va briser les choses.

FWIW, c'est la façon de résoudre ce problème mauvais . Tout algorithme qui vous oblige à regarder 1 point d'avance dans un générateur pourrait également être écrit pour utiliser l'élément générateur de courant et l'élément précédent. Ensuite, vous n'avez pas à mutiler votre utilisation de 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 mise en œuvre d'une classe basée sur la préanalyse suggestion 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 cela, 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, il est une mauvaise idée d'appeler PEEK plusieurs fois de suite ...

En regardant le code source CPython je viens de trouver une meilleure façon qui est à 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 le même que ci-dessus mais vous ne serez pas payer un prix ici pour utiliser PEEK plusieurs fois de suite. Avec quelques lignes, vous pouvez aussi regarder vers l'avenir plus d'un élément dans la iterator (jusqu'à RAM disponible).

Au lieu d'utiliser des éléments (i, i + 1), où 'i' est l'élément en cours et i + 1 est la version 'coup d'oeil avant', vous devriez utiliser (i-1, i), où « i -1' est la version précédente de la génératrice.

Peaufiner votre algorithme de cette façon produira quelque chose qui est identique à ce que vous avez actuellement, en dehors de la complexité inutile supplémentaire d'essayer de « jeter un regard avant ».

est en avance Peeking une erreur, et vous ne devriez pas le faire.

Cela fonctionne - il tamponne un élément et appelle une fonction à chaque élément et l'élément suivant dans la séquence

.

Vos besoins sont glauque ce qui se passe à la fin de la séquence. Que signifie « regarder vers l'avenir » 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

Une solution simple est d'utiliser une fonction comme ceci:

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

Si quelqu'un est intéressé, et s'il vous plaît me corriger si je me trompe, mais je crois qu'il est assez facile d'ajouter un peu de repoussage fonctionnalité à tout iterator.

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

Bien que itertools.chain() est l'outil naturel pour le travail ici, méfiez-vous des boucles comme ceci:

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

... Parce que cela consomme une quantité de plus en plus linéaire de la mémoire, et éventuellement arrêteraient. (Ce code semble essentiellement de créer une liste chaînée, un noeud par chaîne () appel.) Je sais que ce pas parce que je visitai les libs, mais parce que cela vient a donné lieu à un ralentissement important de mon programme - se débarrasser de la ligne de gen = itertools.chain([peek], gen) accélérai il à nouveau. (Python 3.3)

@ jonathan-hartley répondre:

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 facile de créer une classe qui fait cela sur __iter__ et les rendements que l'élément prev et mettre le elm dans un attribut.

WRT de Z @ David après, la nouvelle seekable outil peut réinitialiser un itérateur enveloppé dans 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

cytoolz a un peek fonction.

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top