Iterators are objects that traverses a sequence of elements using what is known as lazy evaluation. This means that an element is only retrieved when it is required.
The most commonly used type of iterators traverses through a given iterable yielding one elements at a time. Basically, this type of iterators are created using the builtin iter()
function.
After the iterator is created we use the builtin next()
function to access the elements one at a time..
Consider the following example:
data = ['Python', 'Java', 'Kotlin']
my_iterator = iter(data)
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
In the above example, we created an iterator i.e my_iterator
for traversing through the elements of the list i.e data
. .Calling the next()
function with the iterator as the argument yields the next element in the sequence. Note that once all the elements are exhausted, the iterator becomes rather unusable and any subsequent calls will lead to the StopIteration
exception being raised.
By themselves, iterators do not store any elements.
data = ['Python', 'Java', 'Kotlin']
my_iterator = iter(data)
print(len(my_iterator))
As shown above, trying to get the length of an iterator raises an exception because the iterator does not have any elements stored in it. It is actually more accurate to view an iterator like the one above as if it is a mere tool for scanning through the elements of the target iterable. The following example demonstrates this better.
data = ['Python', 'Java', 'Kotlin']
my_iterator = iter(data)
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
data.append('C++')
print(next(my_iterator))
The above example proves that iterators simply scans through the elements of the referenced list one at a time. Note that when we added a new item in the list later, the new element is also yielded by the iterator. This is because the iterator is actually referencing the original list rather than storing the elements itself.
With that, we are now ready to understand what infinite iterators are and how they work.
What are infinite iterators?
An infinite iterator is any iterator that can go on yielding values indefinitely. This means that the iterator will never raise the StopIteration
exception because there will always be a value to be yielded.
We can create infinite iterators from scratch but before we look on how to do this, let us first look at builtin infinite iterators.
The itertools
module in the standard library provide three tools for creating infinite iterators, these are:
cycle
count
repeat
itertools.cycle
We saw how we can use the iter()
function to create an iterator that scans through the elements of an iterable from left to right. itertools.cycle()
works similarly except that when the end of the target iterable is reached a StopIteration
exception is not raised, instead, iteration moves back to the beginning of the iterable and this goes on indefinitely.
Consider the following example:
import itertools
data = ['Python', 'Java', 'Kotlin']
obj = itertools.cycle(data)
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
As shown above, the created iterator object starts again when the end of the list is reached.
itertools.count
The count
iterator generates infinite numbers given a starting point and the skip/step value. It has the following syntax:
itertools.count(start = 0, step = 1)
Both start
and step
are optional, their default values are 0
and 1
, respectively.
import itertools
nums = itertools.count()
print(next(nums))
print(next(nums))
print(next(nums))
print(next(nums))
print(next(nums))
print(next(nums))
#this can go on indefinitely
In the above example, we called count()
without any argument and therefore, the default values for start
and step
are used. In the following example, we provide our own values for start
and stop
.
import itertools
nums = itertools.count(start = 5.0, step = 2.5)
print(next(nums))
print(next(nums))
print(next(nums))
print(next(nums))
print(next(nums))
print(next(nums))
#this can go on indefinitely
In the above example, the sequence begins at 5.0
and 2.5
is the increment value.
itertools.repeat
itertools.repeat
creates an iterator that repeats a given object a specified number of times. Its syntax is as shown below:
itertools.repeat(value, times = None)
The given value will be repeated the specified number of times or indefinitely if the times parameter is not specified. Consider the following example:
import itertools
obj = itertools.repeat('Python')
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
#this can go on indefinitely
In the above example, we did not specify a value for the times parameter, the repetition can therefore go on indefinitely.
How to Create Custom Infinity Iterators
Iterators in Python are required to implement two mandatory methods:
__iter__()
__next__()
The __iter__()
method is called when initializing the iterator, it should return the iterator itself.
The __next__()
method retrieves the next value in the sequence. It is also responsible for defining the logic of terminating the iterator by raising the StopIteration
exception when iteration is over.
class Countdown:
def __init__(self, start, stop = 0):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current < self.stop:
raise StopIteration
value = self.current
self.current-=1
return value
c = Countdown(5)
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
In the above example we created a Countdown
iterator which starts with a value and moves down with each iteration until the stop
value is reached.
The __next__()
method raises a StopIteration
exception when the stop
value is reached, this makes it a definite iterator.
To make Countdown
an infinite iterator, we can remove the stop
value and the StopIteration
logic in __next_()
.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
value = self.current
self.current-=1
return value
c = Countdown(5)
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
print(next(c))
#this can go on indefinitely
In the above example, we created an infinite Countdown
iterator that can go on indefinitely.
Conclusion
- Iterators allows us to traverse through the elements in a sequence one at a time
- Iterators use lazy evaluation meaning that an element is retrieved or computed only when it is needed.
- Infinite iterators are iterators that can go on yielding values indefinitely.
itetools
module in the standard library defines three infinite iterators.itertools.cycle()
iterates through the elements of a given iterable in a loop.itertools.count()
yields infinite numbers from a given starting point and increments with the step value.itertools.repeat()
repeats a given value a specified number of times or indefinitely if number of times is not specified.- We can create custom iterators by defining the
__iter__()
and__next__()
- To define custom infinite iterator, we do not raise a
StopIteration
exception in the__next__()
method