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.