For DevelopersOctober 16, 2024

How to Implement Custom Iterators and Iterables in Python

Discover how to create custom iterators and iterables in Python with a brief guide to the Iterator protocol and practical examples.

One of the key Python tools that let us move through lists, tuples, or dictionaries is iterators and iterables. What if, however, you must design your own unique iterators or iterables? With special examples and use scenarios, this blog will investigate how to apply custom iterators and iterables in Python, thereby offering a comprehensive knowledge of Python Iterator protocol.

Connect with high-paying remote Python jobs through Index.dev's global talent network. Sign up today!

 

Introduction to Iterators and Iterables

Python's simplicity and the power it provides via built-in data structures such as lists, strings, and dictionaries are well recognized. Because these data structures are iterable—that is, you can cycle through them using a for loop. But have you ever pondered the mechanism of this magic?

An iterable is anything having a __iter__() method, which generates an iterator. On the other hand, an iterator implements both the __iter__() and __next__() functions and stands for a data stream.

An easy iteration may be:

numbers = [1, 2, 3, 4]
for number in numbers:
    print(number)

Here, numbers is an iterable, and when you loop through it, Python executes the __iter__() function, followed by __next__().

 

Understanding the Python Iterator Protocol

Python uses the iterator protocol, which is based on two methods:

  • __iter__() returns the iterator object itself.
  • __next__() returns the next item in the sequence. If there are no more items, it throws the StopIteration exception.

Let's create a basic custom iterator that generates numbers 1 through 5.

class MyRange:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= self.end:
            num = self.current
            self.current += 1
            return num
        else:
            raise StopIteration

for number in MyRange(1, 5):
    print(number)

MyRange is a custom iterator. It keeps the current state and increases the value every time __next__() is used. When the iteration approaches the finish, it throws a StopIteration exception.

Read Also: How to Set or Modify a Variable After It’s Defined in Python

 

Implementing a Custom Iterator Class

Now that we have a basic grasp, let's take it a step further and develop a custom iterator that returns even integers inside a given range.

class EvenNumbers:
    def __init__(self, start, end):
        self.current = start if start % 2 == 0 else start + 1
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        even = self.current
        self.current += 2
        return even

# Using the iterator
even_numbers = EvenNumbers(2, 10)
for num in even_numbers:
    print(num)

This custom iterator only generates even integers from 2 to 10. We keep track of the state using the self.current variable, which is incremented by 2 each time __next__() is performed. If the current reaches the limit, the iteration ends.

 

Creating Custom Iterables

Iterables and iterators are not the same thing. Iterators return one value at a time, whereas iterables return iterators. Let us construct a custom iterable that produces Fibonacci numbers.

class FibonacciIterable:
    def __init__(self, max_count):
        self.max_count = max_count

    def __iter__(self):
        return FibonacciIterator(self.max_count)

class FibonacciIterator:
    def __init__(self, max_count):
        self.count = 0
        self.max_count = max_count
        self.a, self.b = 0, 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.count >= self.max_count:
            raise StopIteration
        self.count += 1
        result = self.a
        self.a, self.b = self.b, self.a + self.b
        return result

# Using the iterable
for num in FibonacciIterable(5):
    print(num)

FibonacciIterable produces a custom iterator (FibonacciIterator). This method is handy when you need to generate numerous independent iterators from a single iterable.

 

Practical Use Cases for Custom Iterators and Iterables

Custom iterators and iterables are useful when working with complicated data structures or endless sequences. 

For example:

  • Streaming data: When processing huge datasets in chunks or streams, custom iterators can produce results on demand rather than putting everything into memory all at once.
  • Data filtering: Custom iterators can filter or change data while iterating.

Here's an example of slowly filtering out even values from a range:

class EvenFilter:
    def __init__(self, iterable):
        self.iterable = iterable

    def __iter__(self):
        return self

    def __next__(self):
        for item in self.iterable:
            if item % 2 == 0:
                return item
        raise StopIteration

for even in EvenFilter(range(10)):
    print(even)

 

Combining Iterables with Python's Iterator Tools

The itertools module in Python has a variety of useful functions. Custom iterables can be coupled with itertools to provide more complex capabilities.

import itertools

# Custom iterable of odd numbers
class OddNumbers:
    def __init__(self, start, end):
        self.start = start if start % 2 != 0 else start + 1
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.start > self.end:
            raise StopIteration
        current = self.start
        self.start += 2
        return current

# Using itertools with custom iterable
odd_numbers = OddNumbers(1, 9)
print(list(itertools.islice(odd_numbers, 3)))  # First 3 odd numbers

This demonstrates how custom iterables may be used with itertools to provide slow evaluation, chunking, and other features.

 

Advanced Iterator Techniques

You may take iterators to the next level by adding sophisticated features such as bidirectional iteration or iterator reset.

class BidirectionalRange:
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

    def previous(self):
        if self.current <= self.start:
            raise StopIteration
        self.current -= 1
        return self.current

# Usage
iterator = BidirectionalRange(1, 5)
print(next(iterator))  # Output: 1
print(iterator.previous())  # Output: 1

 

Performance Considerations

When dealing with enormous datasets, memory management becomes crucial. Custom iterators are a fantastic choice for memory optimization since they create values one at a time, which reduces overhead.

For example, when iterating over a large dataset, generators or custom iterators are more efficient than loading the full dataset into memory.

def large_dataset():
    for i in range(1, 1000000):
        yield i

Custom iterators, particularly ones that use slow evaluation, provide significant performance benefits when dealing with data streams or endless sequences.

Read Also: How to Implement a Health Check in Python?

 

Conclusion

Understanding and creating custom iterators and iterables is critical for Python developers, particularly when working with complicated or large-scale data. These approaches provide efficient and scalable data processing, increasing the capabilities of Python's built-in data structures. Custom iterators and iterables can open up new avenues for framework and library development by providing customizable iteration logic.

 

For Python Developers: Join Index.dev and work on high-end remote Python projects worldwide. Sign up now!

For Employers: Need top talent fast? Get matched with senior Python developers in just 48 hours.

Share

Radhika VyasRadhika VyasCopywriter

Related Articles

For EmployersHow Specialized AI Is Transforming Traditional Industries
Artificial Intelligence
Artificial intelligence is changing how traditional industries work. Companies are no longer relying only on general skills. Instead, they are using AI tools and specialized experts to improve productivity, reduce costs, and make better decisions.
Ali MojaharAli MojaharSEO Specialist
For EmployersHow to Scale an Engineering Team After Series A Funding
Tech HiringInsights
Most Series A founders hire too fast, in the wrong order, and regret it by month six. Here's the hiring sequence that actually works, and the mistakes worth avoiding before they cost you a Series B.
Mihai GolovatencoMihai GolovatencoTalent Director