Advanced - Python Developer Interview

Advanced - Python Developer Interview

Iterators, Iterables, Generators

Photo by Kelly Sikkema on Unsplash

Iterators

An iterator is an object representing a stream of data; this object returns the data, one element at a time. The iterator objects themselves are required to support the following two methods, which together form the iterator protocol :

.__iter__() : Called to initialize the iterator. It must return an iterator object.

.__next__() : Return the next item from the iterator. If there are no further items, raise the StopIteration exception.

How to create an Iterator?

We need to implement two methods i.e. .__iter__() and .__next__() to create a custom iterator object:

#example of custom iterator
class SquareIterator:
    def __init__(self, sequence):
        self._sequence = sequence
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index < len(self._sequence):
            square = self._sequence[self._index] ** 2
            self._index += 1
            return square
        else:
            raise StopIteration

for square in SquareIterator([1, 2, 3, 4, 5]):
    print(square)
#output:
1
4
9
16
25

When to use an Iterator in Python?

Iterators come in handy when you need to iterate over a dataset or data stream with an unknown or a huge number of items. In these situations, iterators allow you to process the datasets one item at a time without exhausting the memory resources of your system, which is one of the most attractive features of iterators.

Iterables

An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements sequence semantics.When using iterables, it is usually not necessary to call iter() or deal with iterator objects yourself. The for statement does that automatically for you, creating a temporary unnamed variable to hold the iterator for the duration of the loop

How to create an Iterator from Iterable?

We can create the iterator from iterable using iter() function. Please look into below example:

cities = ["Ba sing se", "Omashu", "Republic city"]

# initialize the object
cities_iterator = iter(cities)

print(next(cities_iterator))
print(next(cities_iterator))
print(next(cities_iterator))

The output will be as below:

Ba sing se
Omashu
Republic city

You can also use for loop with iterator :

cities = ["Ba sing se", "Omashu", "Republic city"]

# initialize the object
cities_iterator = iter(cities)

for item in cities_iterator:
    print(item)

The output will be as below:

Ba sing se
Omashu
Republic city

When we are using for loop then we don't need to worry about the StopIteration exception. It will be taken care of by for loop itself, which means it will exceed the index till the index where we won't receive the StopIteration exception.

Note: Every iterator is also an iterable, but not every iterable is an iterator in Python.For example, a string is an iterable but not an iterator. We can check that from the below code:

name = "Toph"
print(next(name))

The output will give an error:

ERROR!
Traceback (most recent call last):
  File "<string>", line 5, in <module>
TypeError: 'str' object is not an iterator

But if you convert "name" string in iterator using iter() function as shown below:

name = "Toph"
iter_name = iter(name)
print(next(iter_name))
print(next(iter_name))
print(next(iter_name))
print(next(iter_name))

The output will be as below:

T
o
p
h

Now let's move on to our next topic "Generators"

Generators

Generator functions allow you to declare a function that behaves like an iterator, i.e. it can be used in a for loop. The main difference between an iterator and a generator is the "yield" keyword. In normal function, we use the "return" keyword but the generator function requires the "yield" keyword.

Note: "return" keyword will terminate the scope of the function but in the case of the "yield" keyword it will pause the execution of the code until it encounters the next "yield" keyword. Below is the diagram of how the "yield" keyword works:

Below is the example code of the Python generator:

def gen_city():
    cities = ["Ba sing se", "Omashu", "Republic city"]

    # initialize the object  
    for item in cities:
        yield(item)

gen_city_obj=gen_city()
print(gen_city_obj)
for el in gen_city_obj:
    print(el)

The output will be as below:

<generator object gen_city at 0x7fb9fd08b5e0>
Ba sing se
Omashu
Republic city

We can also generate the generators using generator expression, let's see how to do that.

Python Generator Expression:

In Python, a generator expression is a concise way to create a generator object. The syntax of Generator Expression is similar to List Comprehension except it uses parentheses ( ) instead of square brackets [ ]. Example of a generator expression is below:

squares = (i*i for i in range(5))
print(squares)
for n in squares:
    print(n)

The output will be:

<generator object <genexpr> at 0x7efc730e75e0>
0
1
4
9
16

You can see that when we print squares we will see <generator object <genexpr> at 0x7efc730e75e0> which means the generator object is generated with a generator expression.

What we learned in this article:

  • What are iterators

  • When to use iterators in Python

  • What are Iterables

  • How to create an iterator from iterable

  • What are Generators

  • What is Python Generator Expression

References