Iterator

Buffer this pageShare on FacebookPrint this pageTweet about this on TwitterShare on Google+Share on LinkedInShare on StumbleUpon
Reading Time: 3 minutes

Iterator Design Pattern in Python

Iterator Design Pattern in Python

Design Patterns Home
 

What is it?

The Iterator Design Pattern enables the client to sequentially access elements of a composite object, without exposing its internal representation. In Python, the Iterator Pattern consists of providing the client with either

  • a method which can traverse a given object
  • a class whose object can traverse a given object.

Python already has syntax for iterators, so that the for loop works with sequences (strings, lists, dictionaries etc.). So why is there a need for the Iterator Pattern if there is already a language feature for the same? Let's look at the builtin iterator functionality, then we will see why the Iterator Pattern is needed.

Here's a simple example of iterators:

>>> numbersList = [1, 2, 3]
>>> for number in numbersList:
    print(number)

### OUTPUT ###

1
2
3

Sequences and file handlers in Python have an __iter__() magic method. When the sequence is used is in conjunction with a for loop, the __iter__() method of the sequence calls the builtin iter() method and returns an iterator object. This is called the The Iterator Protocol. The iterator object thus received, has a __next__() method, which gives the element in the sequence. When there is no element left and the __next__() method is called, the iterator object raises a StopIteration exception.

>>> numbersListIterator = iter([1, 2, 3])
>>> numbersListIterator
<listiterator object at 0x03040830>
>>> numbersListIterator.__next__()
1
>>> numbersListIterator.__next__()
2
>>> numbersListIterator.__next__()
3
>>> numbersListIterator.__next__()
Traceback (most recent call last):
    numbersListIterator.__next__()
StopIteration

Once you have an iterator object, you can iterate over the values in the following 3 ways:

  1. Using the __next__() magic function of the iterator object, as done above. The __next__() is calling the builtin next() method and passing itself (i.e. numbersListIterator) to it.
  2. Using the builtin next() function explicitly, such as next(numbersListIterator).
  3. Using a for loop, such as for number in numbersListIterator: print(number).

So why is there a need for the Iterator Pattern if there is already a language feature for the same? The Iterator Pattern is implemented to provide the client with a function or a class whose instance performs tasks of an iterator.

In Python, there is no concept of public, private and protected members inside a class, so the part of the definition saying 'without exposing its internal representation' is rendered somewhat futile. Having said that, the Iterator Pattern aims at providing the client with a standard interface.

It is classified under Behavioural Design Patterns as it offers a popular way to handle communication between objects.


Why the need for it: Problem Statement

The Iterator Pattern is implemented to provide the client with a function or a class whose instance performs tasks of an iterator.


How to implement it

We will use the following two concepts to implement our Iterator Pattern:

  1. Constructor of builtin zip class
  2. Generators

The constructor of builtin zip class takes iterables as inputs and returns an iterator of tuples of corresponding elements. The population of the iterator stops when the shortest input iterable is exhausted. Since it returns an iterator, the __next__() method is used fetch the next tuple. to Let's look at a few examples:

# TWO ITERABLES AS INPUTS
>>> zipOne = zip(   [1, 2, 3, 4], [5, 6, 7, 8, 9]  )
>>> zipOne.__next__()
(1, 5)
>>> zipOne.__next__()
(2, 6)
>>> zipOne.__next__()
(3, 7)
>>> zipOne.__next__()
(4, 8)
>>> zipOne.__next__()
Traceback (most recent call last):
    zipOne.__next__()
StopIteration



>>> zipOne = zip(   [1, 2, 3, 4], [5, 6, 7, 8, 9]  )
>>> for element in zipOne:
	print(element)

(1, 5)
(2, 6)
(3, 7)
(4, 8)



>>> zipOne = zip(   range(5), range(5, 10)    )
>>> for element in zipOne:
	print(element)

	
(0, 5)
(1, 6)
(2, 7)
(3, 8)
(4, 9)


>>> zipOne = zip(   range(5), range(5, 10)    )
>>> zipOne.__next__()
(0, 5)
>>> zipOne.__next__()
(1, 6)
>>> zipOne.__next__()
(2, 7)
>>> zipOne.__next__()
(3, 8)
>>> zipOne.__next__()
(4, 9)
>>> zipOne.__next__()
Traceback (most recent call last):
    zipOne.__next__()
StopIteration




# FOUR ITERABLES AS INPUT
>>> zipTwo = zip('Have', 'you', 'met', 'Ted')
>>> for element in zipTwo:
	print(element)

	
('H', 'y', 'm', 'T')
('a', 'o', 'e', 'e')
('v', 'u', 't', 'd')

A Generator is an object in Python which returns a sequence of elements, one at a time. A Generator function returns the Generator object. It is characterized by the keyword 'yield' i.e. a function having the 'yield' keyword in its body is a Generator Function. Basic usage example:

>>> def generateFamousDetectives():
	print("Famous Detective #1:", end = " ")
	yield "Sherlock Holmes"
	print("Famous Detective #2:", end = " ")
	yield "Hercule Poirot"
	print("Famous Detective #3:", end = " ")
	yield "Nancy Drew"

	
>>> generateFamousDetectives
<function generateFamousDetectives at 0x030303D8>

>>> generateFamousDetectives()
<generator object generateFamousDetectives at 0x030290F8>

>>> generatorObjectOne = generateFamousDetectives()
 
>>> generatorObjectOne.__next__()
Famous Detective #1: 'Sherlock Holmes'
>>> generatorObjectOne.__next__()
Famous Detective #2: 'Hercule Poirot'
>>> generatorObjectOne.__next__()
Famous Detective #3: 'Nancy Drew'
>>> generatorObjectOne.__next__()
Traceback (most recent call last):
    generatorObjectOne.__next__()
StopIteration

The generator function generateFamousDetectives returns a generator object, which we can assign to a variable, such as generatorObjectOne. Once we have this object, there are 3 methods in which we can fetch elements from it, just like iterators:

1. Using the __next__() magic function of the generator object, as done above. The __next__() is calling the builtin next() method and passing itself (i.e. the generator object) to it.
2. Using the builtin next() function explicitly, such as next(generatorObjectOne).
3. Using a for loop, such as for detective in generatorObjectOne: print(detective).

To learn more about how Generators work, you can view this article on Generators.

Now, on to the design pattern. We'll create a function as well as a class, an instance of which behaves like a Python iterator. By that, I mean the builtin next() can be called on it to fetch the next element.


ITERATOR FUNCTION

Our aim is to construct a function which takes a number, and returns the English equivalents of all numbers up until the supplied number. This function acts as an iterator, which iterates over the 2-element tuples of the form (1, 'one') etc. and returns the English equivalent of each number.

def countTo(upperBound):
	numbersInEnglish = ["one", "two", "three", "four", "five", 'six', 'seven', 'eight', 'nine']
	customIterator = zip(range(1, upperBound + 1), numbersInEnglish)                                     # (1, 'one')(2, 'two')(3, 'three')etc.
	for numericForm, englishForm in customIterator:
		yield englishForm 

countTo3Iterator = countTo(3)
print(next(countTo3Iterator))
print(next(countTo3Iterator))
print(next(countTo3Iterator))

### OUTPUT ###
one
two
three

ITERATOR CLASS

Our aim is to devise a class whose instance acts as an iterator. Our class accepts a number, and returns the English equivalents of all numbers up until the supplied number. To make our class an iterator, it needs to have the __iter__() and __next__() methods defined, so that it can be treated like an iterator by Python.

class CountTo:
    def __init__(self, upperBound):
        self.upperBound = upperBound
        self.numbersInEnglish = ["one", "two", "three", "four", "five", 'six', 'seven', 'eight', 'nine']
        self.numbersInEnglish = self.numbersInEnglish[:upperBound]              # trims the numbersInEnglish list to as many elements as the number supplied.
        self.index = 0                                                          # index variable needed to keep track of the current position of the iterator in numbersInEnglish list.
    def __iter__(self):
        return self
    def __next__(self):
        try:
            result = self.numbersInEnglish[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return result

countTo3 = CountTo(3)
print(next(countTo3))
print(next(countTo3))
print(next(countTo3))


### OUTPUT ###
one
two
three

Related to: Composite


 

 

 

 


See also:

Buffer this pageShare on FacebookPrint this pageTweet about this on TwitterShare on Google+Share on LinkedInShare on StumbleUpon

Leave a Reply