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:
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:
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:
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:
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