Introduction
Iterators are objects that allow to traverse through all the elements of a collection, regardless of its specific implementation. Iterator object must implement two special methods, __iter__()
and __next__()
, collectively called the iterator protocol. An object is called iterable if we can get an iterator from it. Most of built-in containers like: list, tuple, string etc. are iterables. The iter()
function (which in turn calls the __iter__() method) returns an iterator from them.
Use the next() function to manually iterate through all the items of an iterator. When there is no more data to be returned, it will raise StopIteration.
Syntax
iter() function syntax is
iter(object[, sentinel]
It return an iterator object. Object must be a collection object which supports the iteration protocol, or it must support the sequence protocol. If the second argument (sentinel) is given, then object must be a callable object. Iterator created in this case will call object with no arguments for each call to its next() method. If the value returned is equal to sentinel, StopIteration will be raised, otherwise the value will be returned.
Below example demonstrate the use of iter() function using List.
# Define list my_list = [4, 7, 0, 3] # Get iterator my_iter = iter(my_list) # Iterate through it using next() #Output: 4 print(next(my_iter)) #Output: 7 print(next(my_iter)) #Output: 0 print(my_iter.__next__()) #Output: 3 print(my_iter.__next__()) # This will raise error, no items left next(my_iter)
Loop with Iterators
for
loop can iterate over any iterable. Internally, the for loop creates an iterator object, iter_obj by calling iter() on the iterable. Inside the loop, it calls next() to get the next element and executes the body of the for loop with this value. After all the items exhaust, StopIteration is raised which is internally caught and the loop ends.
for element in iterable: # do something with element # Behind the scene for the above loop # Create an iterator object from that iterable iter_obj = iter(iterable) # infinite loop while True: try: # get the next item element = next(iter_obj) # do something with element except StopIteration: # if StopIteration is raised, break from loop break
Iterator with Sentinel
When a sentinel value is supplied to iter(), it will assume that first argument is callable (without arguments). iter() will call callable repeatedly until it returns the sentinel value. Afterwards, the iterator would stop. Sentinel value makes it possible to detect the end of the data.
One useful application of the second form of iter() is to build a block-reader. For example:
blocks = [] read_block = partial(f.read, 32) for block in iter(read_block, ''): blocks.append(block)
The sentinel value in this case is an empty string. read_block takes no input and reads a constant size.
Building Own Iterator
To build an iterator from scratch we have to implement the methods __iter__() and __next__(). The __iter__() method returns the iterator object itself. The __next__() method must return the next item in the sequence. On reaching the end, and in subsequent calls, it must raise StopIteration.
class PowTwo: """Class to implement an iterator of powers of two""" def __init__(self, max = 0): self.max = max def __iter__(self): self.n = 0 return self def __next__(self): if self.n <= self.max: result = 2 ** self.n self.n += 1 return result else: raise StopIteration # Using above iterator for i in PowTwo(5): print(i) # Output # 1 # 2 # 4 # 8 # 16 # 32
Generators
Generators is an easier way to create iterators using a keyword yield from a function. Inside the while loop when it reaches to the yield statement, the value of low is returned and the generator state is suspended. During the second next call the generator resumed where it freeze-ed before and then the value of low is increased by one. It continues with the while loop and comes to the yield statement again.
def counter_generator(low, high): while low <= high: yield low low += 1 for i in counter_generator(5,10): print(i, end=' ')