Iterators and Generators

Iterators and generators are key tools in Python that enable efficient iteration over data.


Iterators

An iterator is an object that can be iterated upon, meaning you can traverse through all the values. It implements two special methods:

  • __iter__(): Returns the iterator object itself.
  • __next__(): Returns the next value and raises StopIteration when there are no more items.

Example

# Creating an iterator from a list
numbers = [1, 2, 3]
iterator = iter(numbers)

print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3
Pitfall

If you call next() on an iterator after it is exhausted, it raises a StopIteration exception.

Custom Iterator

You can create a custom iterator by defining a class with __iter__() and __next__() methods.

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

    def __iter__(self):
        return self

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

counter = Counter(1, 5)
for num in counter:
    print(num)

Generators

A generator is a simpler way to create iterators. Instead of using __iter__() and __next__(), you define a function and use the yield keyword to yield values one at a time.

Example

# A simple generator

def my_generator():
    yield 1
    yield 2
    yield 3

for value in my_generator():
    print(value)

Generator Expression

Generators can also be created using a generator expression, similar to list comprehensions but with parentheses.

squares = (x**2 for x in range(5))
for square in squares:
    print(square)
Info

Generators are memory-efficient because they yield values one at a time instead of storing all values in memory.


Differences Between Iterators and Generators

FeatureIteratorGenerator
CreationRequires __iter__ and __next__ methodsDefined using a function and yield keyword
Memory UsageCan store all data in memoryYields one item at a time (lazy evaluation)
ComplexityMore complex to implementSimpler and more concise

Use Cases

  1. Iterators:
    • Implementing complex data structures that need custom iteration logic.
  2. Generators:
    • Processing large data streams (e.g., reading large files).
    • Infinite sequences like Fibonacci series or random number generation.
Task

Practice: Build and Use Iterators and Generators

  1. Custom Iterator:

    • Create a custom iterator for even numbers up to a given limit.

    Example:

    class EvenNumbers:
        def __init__(self, max):
            self.current = 0
            self.max = max
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.current > self.max:
                raise StopIteration
            else:
                self.current += 2
                return self.current - 2
    
    for num in EvenNumbers(10):
        print(num)
    
  2. Simple Generator:

    • Create a generator that yields square numbers up to a limit.

    Example:

    def squares(limit):
        for i in range(limit):
            yield i**2
    
    for square in squares(5):
        print(square)
    
  3. Generator Expression:

    • Write a generator expression to create the first 10 Fibonacci numbers.

    Example:

    def fibonacci(n):
        a, b = 0, 1
        for _ in range(n):
            yield a
            a, b = b, a + b
    
    for fib in fibonacci(10):
        print(fib)
    

Copyright © 2025 Devship. All rights reserved.

Made by imParth