Skip to content

Iterators, Generators and Iterables

Iterables

Lists, tuples, dictionaries, sets, strings - are ITERABLES objects, that means that they implements __iterate__ and __next__ methods, and you cat iterate through its elements.

Iterator is a part of iterable, which you create using the iter() method.

Iterators

You can make ITERATOR out of ITERABLE objects (they implement such methods):

fruits_tuple = (melon, strawberry, peach)
fruits_iter = iter(fruits_tuple)
print(next(fruits_iter))
print(next(fruits_iter))
print(next(fruits_iter))

The next() method can be used only with iterators. It is used to fetch the next value of the iterable, whenever we use the print() function with the next().

Both iterables and iterators can use the iter() method that fetches an iterator.

The for loop basically creates an iterator and implements the next()method for each iteration.

In this case FOR is the same as WHILE

Creating iterator

class Sequence:
    def __init__(self):
        self.num = 1

    def __iter__(self):
        return self

    def __next__(self):
        value = self.num
        if value >= 9:
            raise StopIteration
        self.num += 2
        return value

ite = Sequence()
print(next(ite))
print(next(ite))
print(next(ite))
print(next(ite))
print(next(ite))

Output:

1
3
5
7
StopIteration

The __next__() and __iter__() methods make this class an iterator. The __iter__() method fetches and initiates the iterator. The Sequence class is an iterator, thus it returns itself.

The __next__() method fetches the current value from the iterator and moves to the next state while the next call. We update the num variable by 2 to the output of odd numbers. Here we also mention when the sequence should raise the StopIteration error.

when you iter(EachSecondElement_object) iter method calls and self.current_element becomes 1

Generators

Generators provide a better way to create iterators in Python. You can define a proper function using the yield statement instead of the returnstatement. For example:

def subjects():
    yield "machine learning"
    yield "business analytics"
    yield "java"
    yield "python"


subjects_library = subjects()
print(next(subjects_library))
print(next(subjects_library))
print(next(subjects_library))
print(next(subjects_library))
print(next(subjects_library))

Output:

machine learning
business analytics
java
python
StopIteration

The yield keyword is similar to the return statement but with some additional functionality — it actually remembers the state of the function. So next time the generator is called, it won’t start from the start but rather from where it was last called.

Generator expressions are better at generating sequences and are memory-efficient. They are often compared to list comprehensions as the way the code is written for both is similar:

gen = (x ** 2 for x in range(4))
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))

Output:

0
1
4
9
StopIteration

Random generator example:

def dice_player(n_rounds: int) -> Generator[int, None, None]:
    for i in range(n_rounds):
        yield random.randint(1, 6)

Usage:

player = dice_player(3)

print(next(player))  # 3
print(next(player))  # 1
print(next(player))  # 5
print(next(player))  # StopIteration

Fibbonaci generator:

def fibonacci_generator(num: int) -> int:
    iteration = 0
    prev_num = 0
    next_num = 1
    while True:
        if iteration == num:
            break
        yield prev_num
        prev_num, next_num = next_num, prev_num + next_num
        iteration += 1


fib = fibonacci_generator(10)  
for i in fib:  
    print(i)

Output:

0
1
1
2
3
5
8
13
21
34

Complex generator inside iterator example (closest prime number and reverse iteration)

from __future__ import annotations  
from typing import Generator  

class LowerPrime:  
    def __init__(self, number: int) -> None:  
        self.number = number  

    def __iter__(self) -> LowerPrime:  
        self.iteration = 0  
        self.prime_numbers_generator = LowerPrime.prime_generator()  
        self.closest_prime = 0  
        self.previous = 0  
        self.prime_list = []  

        for prime in self.prime_numbers_generator:  
            if prime >= self.number:  
                self.closest_prime = self.previous  
                break  
            self.prime_list.append(prime)  
            self.iteration = len(self.prime_list)  
            self.previous = prime  
        return self  

    def __next__(self) -> int:  
        if self.iteration == 0:  
            raise StopIteration  
        while self.iteration >= 0:  
            self.iteration -= 1  
            return self.prime_list[self.iteration]  

    @staticmethod  
    def prime_generator() -> Generator[int, None, None]:  
        prime_number = 2  
        while True:  
            yield prime_number  
            while True:  
                prime_number += 1  
                if LowerPrime.__is_prime__(prime_number):  
                    break  
    @staticmethod    def __is_prime__(number: int) -> bool:  
        is_prime = True  
        for i in range(2, number):  
            if number % i == 0:  
                return not is_prime  
        return is_prime