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