'How to take first element from iterator/generator and put it back in Python?
I would like to take first element from iterator, analyse it, then put it back and work with iterator as if it was not touched.
For now I wrote:
def prepend_iterator(element, it):
yield element
for element in it:
yield element
def peek_first(it):
first_element = next(it)
it = prepend_iterator(first_element, it)
return first_element, it
first_element, it = peek_first(it)
analyse(first_element)
continue_work(it)
it is possible to write better/shorter?
Solution 1:[1]
Here is an example wit itertools.tee
import itertools
def colors():
for color in ['red', 'green', 'blue']:
yield color
rgb = colors()
foo, bar = itertools.tee(rgb, 2)
#analize first element
first = next(foo)
print('first color is {}'.format(first))
# consume second tee
for color in bar:
print(color)
output
first color is red
red
green
blue
EDIT (05 May 2022): To expand this answer, if you don't mind installing extra [third-party] package, there is more-itertools, that provide convenient tools to lookahead and lookback when working with iterator. These tools peek at an iterable’s values without advancing it.
Solution 2:[2]
Here I present a simple method that exploits concatenation of generators.
import itertools
def concat_generators(*args: Iterable[Generator]) -> Generator:
r"""
Concat generators by yielding from first, second, ..., n-th
"""
for gen in args:
yield from gen
your_generator = (i for i in range(10))
first_element = next(your_generator)
# then you could do this
your_generator = concat_generators([first_element], your_generator)
# or this
your_generator = itertools.chain([first_element], your_generator)
Solution 3:[3]
Note this will only work if you're pushing back non-None values.
If you implement your generator function (which is what you have) so that you care about the return value of yield, you can "push back" on the generator (with .send()):
# Generator
def gen():
for val in range(10):
while True:
val = yield val
if val is None: break
# Calling code
pushed = false
f = gen()
for x in f:
print(x)
if x == 5:
print(f.send(5))
pushed = True
Here, you're printing both the x from the for loop and the return value of .send() (if you call it).
0 1 2 3 4 5 5 # 5 appears twice because it was pushed back 6 7 8 9
This will only work let you push back once. If you want to push back more times than that, you do something like:
# Generator
def gen():
for val in range(10):
while True:
val = yield val
if val is None: break
# Generator Wrapper
class Pushable:
def __init__(self, g):
self.g = g
self._next = None
def send(self, x):
if self._next is not None:
raise RuntimeError("Can't pushback twice without pulling")
self._next = self.g.send(x)
def __iter__(self):
try:
while True:
# Have to clear self._next before yielding
if self._next is not None:
(tmp, self._next) = (self._next, None)
yield tmp
else:
yield next(self.g)
except StopIteration: return
# Calling code
num_pushed = 0
f = Pushable(gen())
for x in f:
print(x)
if (x == 5) and (num_pushed in [0,1,2]):
f.send(x)
num_pushed += 1
Produces:
0 1 2 3 4 5 # Pushed back (num_pushed = 0) 5 # Pushed back (num_pushed = 1) 5 # Pushed back (num_pushed = 2) 5 # Not pushed back 6 7 8 9
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Luca Di Liello |
| Solution 3 |
