Python iterable and iterator a tutorial

what is an iterable ?

In python , objects are abstraction of data , they have methods that work with data , and help us to manipulate it . If we take a look at a list , and see all of its methods

>>> import json

>>> _list = []
# an empty list

>>> json_list = json.dumps(dir(_list), sort_keys=True, indent=4)
# dump the methods and properties of a list to a json object

>>> print(json_list)
[
    "__add__",
    "__class__",
    "__contains__",
    "__delattr__",
    "__delitem__",
    "__dir__",
    "__doc__",
    "__eq__",
    "__format__",
    "__ge__",
    "__getattribute__",
    "__getitem__",
    "__gt__",
    "__hash__",
    "__iadd__",
    "__imul__",
    "__init__",
    "__init_subclass__",
    "__iter__",
    "__le__",
    "__len__",
    "__lt__",
    "__mul__",
    "__ne__",
    "__new__",
    "__reduce__",
    "__reduce_ex__",
    "__repr__",
    "__reversed__",
    "__rmul__",
    "__setattr__",
    "__setitem__",
    "__sizeof__",
    "__str__",
    "__subclasshook__",
    "append",
    "clear",
    "copy",
    "count",
    "extend",
    "index",
    "insert",
    "pop",
    "remove",
    "reverse",
    "sort"
]
# print the json object

we can see that we can add elements to a list using add , and that we can remove an element from a list using remove , and we can also see that we have some methods which start with a double underscore . These methods are called special methods , and they are implemented by default on some of python data types such as list , and dictionaries ..

in python everything is an object , an integer a string , a double , a function a class .. they are all objects . A instance of a class is also called an object . An object has a type , it has data [properties , attributes] , and it has some methods . An iterable object is an object that defines a way to iterate over its data .

But what does iterate mean ? Iterate simply means to loop over each of its elements , and do something with , like printing or calculating the sum , or whatever. Let us give an example of an iterable , let us say we have some electronics in our house , and we want to store them in python , where should we store them ?

We can store them in a list . A list is just a collection of elements , that we can iterate over , hence we can access the elements that we have stored through looping .

# what is an iterable ?
# want to print the gadgets that we have
# store them in an iterable ?
#   yes
#       ?
#       -> an iterable enable looping through an object

gadgets = ['i7', 's8', 'samsung screen']  
# store the gadget in a list , which is an iterable
# to loop through the gadgets , because we want to print
# them

for gadget in gadgets:  
    # print the gadget
    print(f'{gadget}')

# output
i7
s8
samsung screen

Let us give another example , let us say , we have a list of pencil boxes , and each pencil box has a price . We want to store the pencil boxes with their price . Which python type should we use ? The answer is a dictionary : a dictionary is formed of a key and a value , and it helps us to loop through the data .

>>> pencilBoxes = {'pencilBoxA': 2.3, 'pencilBoxB': 1.5}
#  pencil boxes ?
#  price ?
#   {pencilBoxA : price }
#       ~ pencilBoxA : name of the pencil box
#       ~ price      : The price of the pencil in $


>>> print(f'The sum of the prices of the Pencil Boxes is :'
      f' {sum(pencilBoxes.values())}'
      # we use values to get the values inside the dictionary
      )

The sum of the prices of the Pencil Boxes is : 3.8
# calculate total Price

What is an iterator ?

As we have seen in the previous section , an iterable is an object that define a way to loop over its data , so its properties or attributes or methods .

An iterator is an object that enable us to traverse the data of an iterable. An object might define more than one way to loop over its data , hence one or more iterator .

Let us give an example . we want to search for something by using google . for example we want to search for some slides , as such we enter the keyword slide , in google search .

Google has all the data that is related to the keyword slide , this is the search object slide . The search object slide is an iterable since it defines a way to loop over its data . Google provide various iterators to traverse the search object slide , we can traverse it and filter it by date , by language ..

What is the iter function ?

The iter function is used to get an iterator from an object in python . It is used by the for loop , the sum … to get an iterator which is used to loop over the data . the iter function syntax is the following

iter(iterable) -> iterator
iter(callable, sentinel) -> iterator

How to create an iterator in python

In python everything is an object , an object has a type , it can can have some data and some methods . An iterable is an object which type defines a way to loop over its data by the use of an iterator which is an object that enable us to traverse the data of an iterable .

we can get an iterator from an iterable object in python through the use of the iter method .

if an object is an instance of a class

    • we can implement in this class the __iter__ method . The __iter__ method must return an instance of a class that has the __next__ method. The __next__method returns the next data or raises a StopIteration exception when there is no more data to get . The iterator is gotten from this object by using iter(object)
    • we can implement the __getitem__ method . __getitem__ returns data based on a provided index , the index will start from zero . __getitem__ raises a IndexError , when there is no more data. The iterator is gotten by using iter(obj)

if the object is a function or is a callable .

      • A callable is an instance of a class that implements the __callable__ method , so a callable is an instance of a class that can be called like a regular function , a regular function is callable .
      • we can get an iterator from this object by using the iter(callable, sentinel) -> iterator method . This method takes the callable , and call it without providing any value , until the value returned by this callable is equal to the sentinel . A sentinel is just a regular value to stop the iteration .When the returned value is equal to the sentinel , the iterator will raise a StopIteration , to stop the iteration over this iterable . The iterable in this case is the function that we have provided , the iterator is created by using iter which specify when to stop the iteration through the sentinel .

Object is instance of a class

implement the __iter__ and __next__ methods

If we want to create an iterable from an object which is an instance of a class , we must define the __iter__ method . The __iter__ method is called by the iter function , and it must return an iterator . An iterator is simply an object which implements the __next__ method .

class Loop:

    def __init__(self, maxLoop=5):
        self.maxLoop = maxLoop

    def __iter__(self):
        self.loop = 0  
        # reset the iterator when __iter__ 
        # is called
        return self

    def __next__(self):
        if self.loop >= self.maxLoop:
            raise StopIteration
        else:
            self.loop += 1  
            return self.loop

Let us say we want to create the loop object , that iterate over some numbers. The loop object is an instance of the Loop class which as such is an iterable .

The Loop class , defines the __iter__ function which creates a loop counter which starts at zero , and return the instance of this class itself . The iter function will call the __iter__ function in order to get an iterator . This __iter__function returns the class instance itself , as such on this class we have defined the __next__ method which returns the next data , in this case the data from 1 till 5 .

In order to get the data from the loop object , we can either use it with the next method.

>>> loopIterable = Loop()
>>> next(loopIterable)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in __next__
AttributeError: 'Loop' object has no attribute 'loop'
# to use an class that has an iterator , 
#  with next method , we must first 
# create the iterator , or else this will 
# throw an error

>>> iterator_loop = iter(loopIterable)
# create an iterator from loopIterable
# we can now use next with either iterator_loop
# or with loopIterable

>>> next(iterator_loop)
1

>>> next(loopIterable)
2

>>> next(iterator_loop)
3

>>> next(loopIterable)
4

>>> next(loopIterable)
5

>>> next(loopIterable)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in __next__
StopIteration
# once we have iterated through all the elements
# the next function will raise a StopIteration 
# if we want to reuse the next function we must 
# recreate an iterator

>>> iter(loopIterable)
<__main__.Loop object at 0x104e2b050>
>>> next(loopIterable)
1
>>> next(iterator_loop)
2

or we can use it with a for loop or with a list .. this will automatically call the iter function in order to get an iterator from the iterable .

>>> loopIterable = Loop()
>>> iterator_loop = iter(loopIterable)

>>> next(loopIterable)
1

>>> list(loopIterable)
[1, 2, 3, 4, 5]
# this will automatically call iter(loopIterable)
# as such we restart the looping

>>> next(loopIterable)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in __next__
StopIteration
#  there are no more items in the iterable 
# as such next raises an error

for data in loopIterable : 
    print(data , end='')
    if data == 2:
        print()
        break
# output : 12 
# the for loop will call the iter function
# to create an iterator from the iterable 
# we print the first two elements in the 
# iterator and break 

>>> next(loopIterable)
3
# we can use next to continue iterating
# through the iterator

>>> list(loopIterable)
[1, 2, 3, 4, 5]
# if we call list with the iterable 
# it will call the iter function 
# to get an iterator , and we start 
# iterating from the start of the iterable

implement the __getitem__ methods

The iter function can also create an iterator from an object that has the __getitem__ method .

The __getitem__ methods simply returns an element from an object based on a numerical index , if the numerical index does not exist , it raises an IndexError .

The iter function will create an iterator from an instance of a class that has a __getitem__ method . it will pass the indexes starting 0 till an IndexError is raised , in this case it will raise a StopIteration error .

class Paper:
    def __getitem__(self, key):
        '''
            a key can be between 0 and 2
                or
            a key can be larger than 7
        '''
        if 0 <= key < 2 or key > 7:
            return key
        raise IndexError

>>> paper = Paper()

>>> list(paper)
[0, 1]
# the iter function will create an iterator
# for data 0 , 1 only

>>> paper[8]
8
# paper have data which exists at index 
# larger than 7  , but the iter function
# will stop at the first IndexError that 
# is raised

>>> next(paper)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Paper' object is not an iterator
# we cannot use the paper instance
# with next  , 

>>> iter_paper = iter(paper)
# create an iterator from paper

>>> next(iter_paper)
0

>>> next(paper)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Paper' object is not an iterator
# we cannot use the paper instance
# with next  , 


>>> next(iter_paper)
1

>>> next(iter_paper)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
# when there are no more elements 
# a StopIteration exception is raised

object is a Function

We can use the iter function with a function and a sentinel value in order to create an iterator . The sentinel value is used to stop the iteration .

import random
# import the random module

def choice():
    # return a random choice from the list "yes" , "no"  ,"perhaps"
    return random.choice(["yes", "no", "perhaps"])

choice_iter = iter(choice , "no")
# create an iterator , which will stop 
# when the value returned by the choice
# function is no 

>>> list(choice_iter)
['yes', 'perhaps', 'yes', 'perhaps', 'yes', 'yes', 'yes']
list(choice_iter) #[]

>>> list(choice_iter)
[]
# once the iterator is used , we cannot reuse it
# we must create a new one


>>> choice_iter = iter(choice , "no")
>>> list(choice_iter)
['yes']

object is a callable

a callable is an object which is an instance from a class that implements the __call__ method. it can be used as a regular function .

we can use the iter function with a callable , and a sentinel value , the sentinel value is used to stop the iteration .

import random 

class RandomWords:
    ''' return some random words
    '''

    def __init__(self):
        self.words = ["statement", "has", "no", "effect", "stop"]

    def __call__(self):
        return random.choice(self.words)

randomWords = RandomWords()

>>> randomWords = RandomWords()

>>> iter_random_words = iter(randomWords , "stop")

>>> list(iter_random_words)
['no']
# an iterator can only be used once

>>> list(iter_random_words)
[]

>>> iter_random_words = iter(randomWords , "stop")
>>> list(iter_random_words)
['no', 'no']

Complex iterables & Real world usage

iterables can be used to iterate through data , or to generate data without statically storing it . let us give an example . Let us say that we want to generate data for series , so we decided to create a class to do that , this class can generate data for two series , the harmonic series

and the sum series .

This class name is the Series class and it has two inner classes , the HARMONIC and the SUM class , which are used to return an iterator based on if we want to calculate the sum of harmonic or a sum series .

class Series:
    '''
        return an iterator , that return  the sum
        of a harmonic and a sum series
    '''
    SERIES = ['SUM', 'HARMONIC']
    # the series that we can calculate their sum

    _SUM = "SUM"
    _HARMONIC = "HARMONIC"

    # Name of the series to be set by the user

    def __init__(self, series='SUM'):
        '''default series to return , its iterator
            is the SUM Series'''
        self.series = series

    def __iter__(self):
        # return a SUM or HARMONIC instance
        return getattr(self, self.series)()

    @property
    # series property to set and get the series
    def series(self):
        return self.__serie

    @series.setter
    def series(self, series):
        if series not in Series.SERIES:
            # can only get the sum of series defined in SERIES
            raise ValueError("series must be in SERIES")
        self.__serie = series

    class SUM:
        '''
            Iterator to return the sum of sum series
        '''
        def __init__(self):
            self.n = -1

        def __iter__(self):
            # not necessary if we don't want SUM to be iterable
            return self

        def __next__(self):
            # infinite series no StopIteration
            self.n += 1
            return self.n * (self.n + 1) / 2

    class HARMONIC:
        '''
            Iterator to return the sum of
            HARMONIC series
        '''
        def __init__(self):
            self.currentSum = 0
            self.n = 0

        def __iter__(self):
            # not necessary if we don't want SUM to be iterable
            return self

        def __next__(self):
            # infinite series no StopIteration
            self.n += 1
            self.currentSum += 1 / self.n
            return self.currentSum



>>> serie = Series()
>>> sum_iter = iter(serie)
# default iterator is SUM series iterator
   
>>> serie.serie = serie._HARMONIC 
# set serie to harmonic
>>> harm_iter = iter(serie)
# create a HARMONIC series iterator
    
    
for _r in range(10):
     print(f'SUM({_r}) : {next(sum_iter)} ')
     print(f'HARMONIC({_r}) : {next(harm_iter)} ')

SUM(0) : 210.0 
HARMONIC(0) : 210.0 
SUM(1) : 231.0 
HARMONIC(1) : 231.0 
SUM(2) : 253.0 
HARMONIC(2) : 253.0 
SUM(3) : 276.0 
HARMONIC(3) : 276.0 
SUM(4) : 300.0 
HARMONIC(4) : 300.0 
SUM(5) : 325.0 
HARMONIC(5) : 325.0 
SUM(6) : 351.0 
HARMONIC(6) : 351.0 
SUM(7) : 378.0 
HARMONIC(7) : 378.0 
SUM(8) : 406.0 
HARMONIC(8) : 406.0 
SUM(9) : 435.0 
HARMONIC(9) : 435.0

Iterator vs generator object

an iterator is created by using the iter function , while a generator object is created by either a generator function or a generator expression . A generator object can act like an iterator , and can be used wherever an iterator can be used .