1

I'm learning Python by myself.

I've come across the following

sol=map(pow,[1,2,3],[4,5,6])

sol is an iterator.

When I run consecutively next(sol), I will iterate along the elements of sol, until I get StopIteration error.

However, how can I restart the iteration?

I've tried iter_1=itertools.cycle(sol), but I need to restart sol by running sol=map(pow,[1,2,3],[4,5,6]) and only then iter_1=itertools.cycle(sol).

Is there another way?

An old man in the sea.
  • 1,169
  • 1
  • 13
  • 30
  • Do you really want to repeat the iterator indefinitely ? – yatu Feb 19 '20 at 16:26
  • Does this answer your question? [Can iterators be reset in Python?](https://stackoverflow.com/questions/3266180/can-iterators-be-reset-in-python) – Jan Christoph Terasa Feb 19 '20 at 16:26
  • @yatu It's more of a theoretical question. In practice, I don't. – An old man in the sea. Feb 19 '20 at 16:26
  • You can't, because the iterator (in general) doesn't remember the previous values its `__next__` method returned. – chepner Feb 19 '20 at 16:28
  • @JanChristophTerasa Thoses answers are for when the iterator is a file type, I think. Then, I would use seek method. I'm not sure how to use seek with a map object... – An old man in the sea. Feb 19 '20 at 16:29
  • 1
    `itertools.cycle` is indeed the answer. Try this `c = cycle(map(pow,[1,2,3],[4,5,6]))`. And then you will have the effect you desire iterating over `c`. – accdias Feb 19 '20 at 16:30
  • @accdias That doesn't really reset the iterator. Given `c`, you have no way of knowing where the "dividing point" between the end of the original iterator and its beginning is, and you can't reset to the beginning from an arbitrary position in the iterator. (But I'm thinking of the broader definition of "reset".) – chepner Feb 19 '20 at 16:32
  • @chepner, Indeed. But it will give the result expected on the OP, don't you agree? – accdias Feb 19 '20 at 16:33
  • @accdias Do I need import some extra module? because when I run your command, I get NameError: name 'cycle' is not defined – An old man in the sea. Feb 19 '20 at 16:34
  • @accdias Depends on the use case; does it matter that you can no longer distinguish between which "phase" of the iteration you are on? Sometimes, you might need (or at least want) to know when you've reached the end of the original iterator and are going back to the beginning; other times, you don't. You also might only want to repeat *once*, not indefinitely. `next(c)` will never raise `StopIteration` or give any indication of when you are at the point where `next(sol)` would have. – chepner Feb 19 '20 at 16:34
  • @Anoldmaninthesea, I'm using Python 3.7.6 here. `itertools` is in the standard library and all I do is `from itertools import cycle` or, if you want to preserve the namespace, `import itertools`. – accdias Feb 19 '20 at 16:37
  • @chepner, I agree. But judging what is described by the OP, it seems it doesn't matter since he just consecutively runs `next(sol)`. So I'm guessing `cycle` works for him in that case. – accdias Feb 19 '20 at 16:40
  • If you need to cache the "elements" of an iterator you obviously just use `list(it)`. `itertools.cycle` is just sugar to do that for you. – Jan Christoph Terasa Feb 19 '20 at 16:40
  • @accdias well it's strange. If I do 'from itertools import cycle' it works. However, if I just do 'import itertools', my jupyter notebook doesn't recognise cycle() – An old man in the sea. Feb 19 '20 at 16:42
  • 1
    You'd need to call `itertools.cycle` if you just `import itertools`, since you just import the module, but the functions in it remain in the (now imported) namespace of `itertools`. – Jan Christoph Terasa Feb 19 '20 at 16:42
  • 1
    @Anoldmaninthesea, that's right. If you do `import itertools` you are preserving the namespace and then you must call `itertools.cycle()` instead. – accdias Feb 19 '20 at 16:43
  • Many thanks everyone! Kind regards ;) – An old man in the sea. Feb 19 '20 at 16:44

2 Answers2

2

Iterating an iterator/generator consumes the values from it (infinite generators being an exception), meaning that they will no longer be available on future iterations (as you've seen). For a typical iterator/generator in Python, the only true way to "restart" it is to re-initialize it.

>>> sol = map(pow, [1, 2, 3], [4, 5, 6])      
>>> list(sol)
[1, 32, 729]
>>> next(sol)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> sol = map(pow, [1, 2, 3], [4, 5, 6])
>>> next(sol)
1

There are ways that you can work with the iterator to make it reusable though, such as with itertools.tee (as mentioned by one of the answers to the question linked by @JanChristophTerasa), or to convert the iterator into a list, which will persist its data.

itertools.tee

>>> from itertools import tee
>>> sol = map(pow, [1, 2, 3], [4, 5, 6])
>>> a, b = tee(sol, 2)  
>>> list(a)
[1, 32, 729]
>>> list(b)
[1, 32, 729]
>>> list(a)
[]

with tee though, both a and b will still be iterators, so you'll have the same problem with them.

Another common way to handle this is with list()

sol = list(map(pow, [1, 2, 3], [4, 5, 6]))
>>> sol
[1, 32, 729]
>>> sol
[1, 32, 729]

Now, sol is a list of values instead of an iterator, which means you can iterate it as many times as you want - the values will remain there. This does mean you can't use next with it (in the sense of next(sol)), but you can get an iterator back from your new list with iter(sol) if you need an iterator specifically.

Edit

I saw itertools.cycle mentioned in the comments, which is also a valid option so I thought I might add some info on it here as well.

itertools.cycle is one of those infinite generators I mentioned at the start. It is still an iterator, but in a way that you'll never run out of values.

>>> from itertools import cycle
>>> sol = map(pow, [1, 2, 3], [4, 5, 6])
>>> infinite = cycle(sol)
>>> for _ in range(5):                        
...     print(next(infinite))
... 
1
32
729
1
32
>>> 

A few notes on this - after iterating infinite N times, it will be positioned after whatever the last value was pulled from it. Iterating it again later will resume from that position, not from the start.

Also, and this is very important, do not iterate an infinite generator in an unbounded fashion, like list(infinite) or for x in infinite:, or you're gonna have a bad time.

b_c
  • 1,202
  • 13
  • 24
  • 3
    I think `itertools.cycle` is a more appropriate answer to this than `itertools.tee`. With `tee` you need to know how many times you want to iterate beforehand (or tee again every time), with `cycle` you can just reuse the iterator as you please. – Jan Christoph Terasa Feb 19 '20 at 16:45
  • @JanChristophTerasa a good point, I added `cycle` to the post – b_c Feb 19 '20 at 16:51
  • @b_c, if you are following the comments and the answers, you will see I've already suggested that. – accdias Feb 19 '20 at 16:52
  • @accidias Your answer went up while I was editing. I included a bit more info to consider when using an infinite generator that I thought would be helpful, but I did give you an upvote for the useful answer. – b_c Feb 19 '20 at 16:56
  • No worries. It was just a heads up call. – accdias Feb 19 '20 at 16:57
2

To achieve the effect you described on the OP, you can use itertools.cycle(). Something like this:

Python 3.7.6 (default, Jan 30 2020, 09:44:41) 
[GCC 9.2.1 20190827 (Red Hat 9.2.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from itertools import cycle
>>> c = cycle(map(pow,[1,2,3],[4,5,6]))
>>> next(c)
1
>>> next(c)
32
>>> next(c)
729
>>> next(c)
1
>>> next(c)
32
>>> 

But take into consideration the comments on the OP before you choose this approach.

accdias
  • 5,160
  • 3
  • 19
  • 31