Frage

Ich kann nicht herausfinden, wie ich in einem Python -Generator ein Element nach vorne schauen kann. Sobald ich aussehe, ist es weg.

Hier ist was ich meine:

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!

Hier ist ein echtes Beispiel:

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

Kann mir jemand helfen, einen Generator zu schreiben, den Sie ein Element vorantreiben können?

War es hilfreich?

Lösung

Die Python -Generator -API ist ein Weg: Sie können keine Elemente zurückschieben, die Sie gelesen haben. Sie können jedoch einen neuen Iterator mit dem erstellen ITERTOOLS -Modul und vorbereiten Sie das Element:

import itertools

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

Andere Tipps

Um der Vollständigkeit zu willen, die more-itertools Paket (Dies sollte wahrscheinlich Teil eines Python -Programmierers sein) enthält a peekable Wrapper, der dieses Verhalten implementiert. Als Code -Beispiel in die Dokumentation zeigt an:

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

Das Paket ist sowohl mit Python 2 als auch 3 kompatibel, obwohl die Dokumentation die Python 2 -Syntax zeigt.

Ok - zwei Jahre zu spät - aber ich bin auf diese Frage gestoßen und fand keine der Antworten auf meine Zufriedenheit. Kam mit diesem Meta -Generator:

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

führt zu:

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

dh Sie haben jeden Moment während des Iterationszugriffs auf das nächste Element in der Liste.

Sie können itertools.tee verwenden, um eine leichte Kopie des Generators zu erstellen. Wenn Sie dann bei einer Kopie nach vorne stehen, wird sich die zweite Kopie nicht auswirken:

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)

Der "Elements" -Gerator wird nicht beeinflusst, wenn Sie "Peeker" belästigen. Beachten Sie, dass Sie das ursprüngliche "SEQ" nicht verwenden sollten, nachdem Sie "Tee" darauf angerufen haben, das die Dinge brechen wird.

Fwiw, das ist das falsch Weg, um dieses Problem zu lösen. Jeder Algorithmus, bei dem Sie 1 Artikel in einem Generator ansehen müssen, kann alternativ geschrieben werden, um den aktuellen Generatorelement und das vorherige Element zu verwenden. Dann müssen Sie Ihre Verwendung von Generatoren nicht merkeln und Ihr Code wird viel einfacher sein. Sehen Sie meine andere Antwort auf diese Frage.

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

Nur zum Spaß habe ich eine Implementierung einer Lookahead -Klasse erstellt, die auf dem Vorschlag von Aaron basiert:

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

Damit funktioniert das Folgende:

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

Mit dieser Implementierung ist es eine schlechte Idee, ein paar Male hintereinander Peek anzurufen ...

Während ich mir den CPython -Quellcode betrachte, fand ich gerade einen besseren Weg, der sowohl kürzer als auch effizienter ist:

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

Die Verwendung ist die gleiche wie oben, aber Sie zahlen hier einen Preis, um Peek viele Male hintereinander zu verwenden. Mit ein paar weiteren Zeilen können Sie auch mehr als einen Artikel im Iterator (bis zum verfügbaren RAM) nach vorne schauen.

Anstatt Elemente (i, i+1) zu verwenden, wobei 'i' der aktuelle Element ist und ich+1 die "Peek Ahead '-Version" ist, sollten Sie (i-1, i) verwenden, wobei' i-1 ' ist die vorherige Version des Generators.

Wenn Sie Ihren Algorithmus auf diese Weise optimieren, wird etwas produziert, das mit dem identisch ist, was Sie derzeit haben, abgesehen von der extra unnötigen Komplexität des Versuchs, „Vorwärts zu schauen“.

Vorwärtszuschauen ist ein Fehler, und Sie sollten es nicht tun.

Dies funktioniert - es puffern ein Element und ruft eine Funktion mit jedem Element und dem nächsten Element in der Sequenz auf.

Ihre Anforderungen sind düster, was am Ende der Sequenz passiert. Was bedeutet "Look voraus", wenn Sie am letzten sind?

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

Eine einfache Lösung besteht darin, eine solche Funktion zu verwenden:

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

Dann können Sie:

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

Wenn jemand interessiert ist, und bitte korrigieren Sie mich, wenn ich falsch liege, aber ich glaube, es ist ziemlich einfach, jedem Iterator einige Push -Back -Funktionen hinzuzufügen.

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

Obwohl itertools.chain() Ist das natürliche Werkzeug für den Job hier, Vorsicht vor Schläden wie folgt:

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

... weil dies eine linear wachsende Menge an Erinnerung verbraucht und schließlich zum Stillstand schleift. (Dieser Code scheint im Wesentlichen eine verknüpfte Liste zu erstellen, einen Knoten pro Kette ().) Ich kenne dies nicht, weil ich die LIBs inspiziert habe, sondern weil dies nur zu einer größeren Verlangsamung meines Programms führte - die loszuwerden - gen = itertools.chain([peek], gen) Linie hat es wieder aufgeregt. (Python 3.3)

Python3 Snippet für @Jonathan-Hartley Antworten:

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)

Es wäre unkompliziert, eine Klasse zu erstellen, die dies tut __iter__ und ergibt nur die prev Artikel und setzen Sie die elm in einem Attribut.

WRT @David Zs Beitrag, je neuer seekable Das Werkzeug kann einen verpackten Iterator auf eine vorherige Position zurücksetzen.

>>> 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 hat ein spähen Funktion.

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top