Python : Iterators vs Generators

In this article we will discuss the differences between Iterators and Generators in Python.

Iterators and Generators both serves similar purpose i.e. both provides the provision to iterate over a collection of elements one by one. But still both are different. Let’s discuss how they are different

Ease of implementation

Making your class Iterable and the creating Iterator class to iterate over the contents of Iterable requires some extra coding.
Let’s understand by an example,

Suppose we have a simple Range class i.e.

class MyRange:
    def __init__(self, start, end):
        self._start = start
        self._end = end

Now we want to iterate over the numbers in this range using the object of our MyRange class i.e.

myrange = MyRange(10, 20)

for elem in myrange:
    print(elem)

It will show error like this,

Traceback (most recent call last):
  File ".../IteratorsGenerators/gen_2.py", line 135, in <module>
    main()
  File ".../IteratorsGenerators/gen_2.py", line 121, in main
    for elem in myrange:
TypeError: 'MyRange' object is not iterable

It returns the error because our MyRange class is not Iterable. Now we need to make our class Iterable and create an Iterator class for it i.e.

class MyRange:
    ''' This class is Iterable'''
    def __init__(self, start, end):
        self._start = start
        self._end = end

    def __iter__(self):
        return MyRangeIterator(self)

Iterator class,

class MyRangeIterator:
    ''' Iterator for class MyRange'''
    def __init__(self, rangeObj):
        self._rangeObj = rangeObj
        self._pos = self._rangeObj._start
    def __next__(self):
        if self._pos < self._rangeObj._end:
            result =  self._pos
            self._pos += 1
            return result
        else:
            raise StopIteration

Now we can iterate over the numbers in range using MyRange class object i.e.

myrange = MyRange(10, 20)

for elem in myrange:
    print(elem)

Output

10
11
12
13
14
15
16
17
18
19

Basically we override __iter__() function in our MyRange class to make it Iterable and the overridden __next__() function in MyRangeIteration class to make it an Iterator.

We can avoid this extra class if use Generator i.e.

Creating Range class with Generator

Instead of making our range class Iterable we can add a generator function in the class that returns a Generator object. This Generator object can be used to iterate over the numbers in range,

class MySecondRange:
    def __init__(self, start, end):
        self._start = start
        self._end = end

    def forwardTraversal(self):
        ''' Generator Function'''
        _pos = self._start
        while _pos < self._end:
            result = _pos
            _pos += 1
            yield result

Now let’s iterate over the numbers in range using MySecondRange class object i.e.

myrange = MySecondRange(10, 20)

for elem in myrange.forwardTraversal():
    print(elem)

Output:

10
11
12
13
14
15
16
17
18
19

So, basically Generators servers the same purpose as Iterators but in less code.

Multiple Generators but Single Iterator

There can be a single Iterator associated with a Iterable class. For example in our Iterable class MyRange, we returned the MyRangeIterator object that iterates the numbers in range from start to end. What if we want to Iterate in reverse or in some other order ?

We can not do that using Iterators because Iterable class returns a single type of Iterator object. But we can do that using Generators.
For example, let’s add two generator functions in our MySecondRange class i.e.

class MySecondRange:
    def __init__(self, start, end):
        self._start = start
        self._end = end

    def forwardTraversal(self):
        ''' Generator Function'''
        _pos = self._start
        while _pos < self._end:
            result = _pos
            _pos += 1
            yield result

    def reverseTraversal(self):
        ''' Generator Function'''
        _pos = self._end - 1
        while _pos >= self._start:
            result =  _pos
            _pos -= 1
            yield result

 

Now using Generator object returned by forwardTraversal(), we can iterate over the numbers in range in forward direction i.e.

myrange = MySecondRange(10, 20)

for elem in myrange.forwardTraversal():
    print(elem)

Output:

10
11
12
13
14
15
16
17
18
19

 

Whereas, using Generator object returned by reverseTraversal(), we can iterate over the numbers in range in backward direction i.e.

myrange = MySecondRange(10, 20)

for elem in myrange.reverseTraversal():
    print(elem)

Output:

19
18
17
16
15
14
13
12
11
10

So, unlike Iterator, with Generators we can iterate over the elements in multiple ways.

Complete example is as follows.

class MyRangeIterator:
    ''' Iterator for class MyRange'''
    def __init__(self, rangeObj):
        self._rangeObj = rangeObj
        self._pos = self._rangeObj._start
    def __next__(self):
        if self._pos < self._rangeObj._end:
            result =  self._pos
            self._pos += 1
            return result
        else:
            raise StopIteration

class MyRange:
    ''' This class is Iterable'''
    def __init__(self, start, end):
        self._start = start
        self._end = end

    def __iter__(self):
        return MyRangeIterator(self)

class MySecondRange:
    def __init__(self, start, end):
        self._start = start
        self._end = end

    def forwardTraversal(self):
        ''' Generator Function'''
        _pos = self._start
        while _pos < self._end:
            result = _pos
            _pos += 1
            yield result

    def reverseTraversal(self):
        ''' Generator Function'''
        _pos = self._end - 1
        while _pos >= self._start:
            result =  _pos
            _pos -= 1
            yield result

def main():

    myrange = MyRange(10, 20)

    for elem in myrange:
        print(elem)

    print('*** Using Generator to Iterate over a range ***')
    myrange = MySecondRange(10, 20)

    for elem in myrange.forwardTraversal():
        print(elem)

    print('*** Using Generator to Iterate in Rerverse over a range ***')
    myrange = MySecondRange(10, 20)
    for elem in myrange.reverseTraversal():
        print(elem)

if __name__ == '__main__':
  main()

Output:

10
11
12
13
14
15
16
17
18
19
*** Using Generator to Iterate over a range ***
10
11
12
13
14
15
16
17
18
19
*** Using Generator to Iterate in Rerverse over a range ***
19
18
17
16
15
14
13
12
11
10

 

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top