Python > Advanced Python Concepts > Iterators and Generators > Iterator Protocol (`__iter__`, `__next__`)

Custom Range Iterator

This snippet demonstrates how to create a custom iterator using the iterator protocol (__iter__ and __next__). We'll build a MyRange class that mimics the behavior of Python's built-in range() function but showcasing the explicit iterator implementation.

Code Implementation

The MyRange class defines a custom range. The __init__ method initializes the start and end values. The __iter__ method returns the iterator object itself (in this case, self). The __next__ method returns the next value in the sequence. When the end is reached, it raises StopIteration, signaling the end of the iteration.

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

    def __iter__(self):
        return self  # MyRange itself is the iterator

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

# Usage:
my_range = MyRange(1, 5)
for num in my_range:
    print(num)

Concepts Behind the Snippet

The iterator protocol in Python consists of two methods: __iter__ and __next__. The __iter__ method must return an iterator object. If the class itself is an iterator, it returns self. The __next__ method must return the next value in the sequence. When there are no more values, it must raise the StopIteration exception. This signals to the for loop or other iterator consumers that the iteration is complete.

Real-Life Use Case

Iterators are valuable when dealing with large datasets or when you need to generate values on demand rather than storing them all in memory at once. For example, imagine reading data from a large file line by line. An iterator can efficiently read and process each line without loading the entire file into memory. Another example is working with infinite sequences, such as generating prime numbers. You can create an iterator that yields prime numbers indefinitely.

Best Practices

  • Ensure that your __next__ method raises StopIteration when there are no more items to return.
  • Consider making your iterator stateful, as the MyRange example shows.
  • For simple iterations, consider using generators (using the yield keyword), as they often lead to more concise and readable code.

Interview Tip

Be prepared to explain the iterator protocol and how it enables efficient iteration over sequences. You might be asked to implement a custom iterator or to identify situations where using iterators would be beneficial. Understanding the difference between iterators and iterables is also crucial. An iterable is something you can get an iterator from, while an iterator is the object that actually does the iterating.

When to Use Them

Use iterators when you need to process large datasets efficiently, generate values on demand, or work with potentially infinite sequences. They help avoid loading large amounts of data into memory at once.

Memory Footprint

Iterators are memory-efficient because they generate values one at a time, rather than storing the entire sequence in memory. This is particularly important when dealing with large datasets.

Alternatives

Generators provide a more concise syntax for creating iterators using the yield keyword. List comprehensions can be used to create lists in a more readable way. Standard looping constructs (for loops) can also be used, but they may not be as memory-efficient when dealing with large datasets.

Pros

  • Memory efficiency: Iterators generate values on demand, avoiding the need to store the entire sequence in memory.
  • Lazy evaluation: Values are computed only when needed, which can save time and resources.
  • Code clarity: Iterators can make code more readable and maintainable by encapsulating the iteration logic.

Cons

  • More complex implementation: Creating custom iterators requires understanding the iterator protocol and implementing the __iter__ and __next__ methods.
  • Stateful: Iterators are stateful, meaning that they maintain their position in the sequence. This can make it more difficult to reason about their behavior.

FAQ

  • What is the difference between an iterator and an iterable?

    An iterable is an object that can return an iterator. It has an __iter__ method that returns a new iterator object. An iterator is an object that generates values on demand. It has a __next__ method that returns the next value in the sequence or raises StopIteration when there are no more values.
  • When should I use a generator instead of a custom iterator class?

    Use a generator when the iteration logic is relatively simple and can be expressed concisely using the yield keyword. Generators are often more readable and easier to maintain than custom iterator classes. Use a custom iterator class when you need more control over the iteration process or when you need to implement more complex logic.