Wie schaue ich in einem Python -Generator nach vorne (Peek)?
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?
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