Python - iterators

In Python, iterators are objects that allow you to traverse through data sequentially, one element at a time. An iterator is helpful when you’re dealing with data streams, large data sets, or need a way to iterate over collections without using indexing, which is not always efficient or possible.

In this post, we’ll discuss:

Understanding Iterators and Iterables

Creating an Iterator in Python

Using Iterators with for Loops

Manually Accessing Elements with next()

Creating Custom Iterators with Classes

Generators as Iterators

Understanding Iterators and Iterables

What is an Iterable?

An iterable is any Python object capable of returning its elements one at a time. Common examples include lists, tuples, dictionaries, sets, and strings. You can loop over an iterable object with a for loop, which under the hood, converts the iterable into an iterator.

What is an Iterator?

An iterator is an object with two key methods:

__iter__(): Returns the iterator object itself.

__next__(): Returns the next item from the collection. Raises a StopIteration exception when no items are left.

To use an iterator, you first need to create it from an iterable using iter() and access its elements with next().

Basic Iterator Example

Let’s start with a simple example using iter() and next():

# Iterable object (list)

my_list = [1, 2, 3, 4, 5]

# Convert to an iterator

my_iterator = iter(my_list)

# Access elements with next()

print(next(my_iterator))  # Output: 1

print(next(my_iterator))  # Output: 2

print(next(my_iterator))  # Output: 3

Each call to next() fetches the next element until the iterator is exhausted, where it will raise a StopIteration exception.

Using Iterators with for Loops

When you use a for loop with an iterable, Python implicitly converts it to an iterator. This means it automatically calls __iter__() to initialize and __next__() in each iteration until StopIteration is raised.

for item in my_list:

    print(item)

This code will output each element in my_list without requiring explicit calls to next() or handling StopIteration.

Manually Accessing Elements with next()

The next() function allows manual control over iteration. Here’s an example using a string:

string = "Python"

string_iterator = iter(string)

print(next(string_iterator))  # Output: 'P'

print(next(string_iterator))  # Output: 'y'

print(next(string_iterator))  # Output: 't'

This method is useful when you want precise control over how an iterable is accessed, such as when processing elements conditionally.

Creating Custom Iterators with Classes

You can create custom iterators by defining a class with __iter__() and __next__() methods. This is particularly useful if you want to encapsulate logic within an iterator.

Example: Countdown Iterator

class Countdown:

    def __init__(self, start):

        self.current = start

    def __iter__(self):

        return self

    def __next__(self):

        if self.current <= 0:

            raise StopIteration

        self.current -= 1

        return self.current + 1

# Using the Countdown iterator

countdown = Countdown(5)

for num in countdown:

    print(num)  # Output: 5, 4, 3, 2, 1

Here, Countdown is an iterator class that counts down from a given start value. It decreases the count with each call to next() until it reaches zero, at which point it raises StopIteration.

Generators as Iterators

Python generators are a simpler way to create iterators using the yield keyword. A function with yield turns into a generator, and each call to next() resumes the function from the last yield statement, returning the yielded value.

Example: Generator for Fibonacci Sequence

def fibonacci(n):

    a, b = 0, 1

    for _ in range(n):

        yield a

        a, b = b, a + b

# Using the Fibonacci generator

for num in fibonacci(5):

    print(num)  # Output: 0, 1, 1, 2, 3

Generators are memory-efficient because they yield one item at a time rather than storing all values in memory.