Skip to main content

Iterators and Generators

Iterators

What is an Iterator?

An iterator is an object that implements two key methods:

  1. __iter__(): Returns the iterator object itself. This method is required for an object to be considered iterable.
  2. __next__(): Returns the next item in the sequence. If there are no more items, it raises a StopIteration exception.

In Python, most collections such as lists, tuples, and dictionaries are iterables. This means We can loop over them using a for loop. However, these collections are not iterators themselves. They are iterable objects because they implement the __iter__() method. When We pass an iterable object to the iter() function, it returns an iterator.

Example of Using an Iterator:

# Creating an iterator for a list
my_list = [1, 2, 3, 4]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator)) # Output: 4
# print(next(iterator)) # This will raise StopIteration

Custom Iterator:

We can create Our own iterator by defining a class with __iter__() and __next__() methods.

class Countdown:
def __init__(self, start):
self.start = start

def __iter__(self):
return self

def __next__(self):
if self.start <= 0:
raise StopIteration
else:
self.start -= 1
return self.start

countdown = Countdown(5)
for num in countdown:
print(num) # Output: 4, 3, 2, 1, 0

Advantages of Iterators:

  • Memory efficient: Iterators yield one item at a time, which makes them more memory-efficient than lists, especially for large datasets.
  • Lazy evaluation: Iterators do not generate all items at once, which allows for processing large data sets one element at a time.

Generators

What is a Generator?

A generator is a special type of iterator that is defined using a function with the yield keyword. Instead of returning a value with return, the generator function produces a sequence of values one at a time using yield. The state of the generator is saved between calls, so the generator function can continue where it left off after each yield.

Example of a Simple Generator:

def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1

counter = count_up_to(3)
print(next(counter)) # Output: 1
print(next(counter)) # Output: 2
print(next(counter)) # Output: 3
# print(next(counter)) # This will raise StopIteration

Generator Expression:

We can also create generators using generator expressions, which have a syntax similar to list comprehensions but with parentheses.

# Using generator expression
squares = (x * x for x in range(5))
for square in squares:
print(square) # Output: 0, 1, 4, 9, 16

Advantages of Generators:

  • Memory Efficient: Like iterators, generators do not store all values in memory at once. Instead, they generate values on the fly, which is especially useful for working with large datasets.
  • Concise: Generators allow We to write cleaner and more concise code for producing sequences of data.
  • Lazy Evaluation: Generators evaluate values lazily, which means values are only generated when requested.

When to Use Generators:

  • When working with large datasets, like reading lines from a file, or streaming data from an external source.
  • When We want to avoid storing large amounts of data in memory at once.
  • When We need an efficient way to handle sequences of data that may not fit in memory.

Comparison Between Iterators and Generators

FeatureIteratorGenerator
CreationExplicitly define __iter__() and __next__()Use a function with yield
StateMaintains its state manuallyAutomatically saves state using yield
Memory UsageCan be memory-intensive for large sequencesMore memory-efficient due to lazy evaluation
SyntaxMore code needed to implementMore concise and readable using yield
PerformanceSlightly slower due to manual state handlingMore efficient as it uses lazy evaluation