Table of Contents
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 .