Iterators & Generators

Iterators & Generators

1. Iterables and Iterators

Concept Distinction

Term Description Examples
Iterable Object with __iter__ method list, str, dict, set
Iterator Object with __iter__ and __next__ methods iter(list), file objects
┌──────────────────────────────────────────┐
│              Iterable                     │
│  ┌────────────────────────────────────┐  │
│  │    Iterator                         │  │
│  │    __iter__() → self               │  │
│  │    __next__() → next value or StopIteration │
│  └────────────────────────────────────┘  │
│  __iter__() → Returns Iterator           │
└──────────────────────────────────────────┘

How for Loops Work

# for item in iterable:
#     ...

# The above code is equivalent to:
iterator = iter(iterable)  # Calls __iter__()
while True:
    try:
        item = next(iterator)  # Calls __next__()
        # Execute loop body
    except StopIteration:
        break

Example

numbers = [1, 2, 3]

# Create iterator with iter()
iterator = iter(numbers)

print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3
# print(next(iterator))  # StopIteration exception!

2. Custom Iterators

Implementing iter and next

class Counter:
    """Iterator that counts from 1 to max"""

    def __init__(self, max_count):
        self.max_count = max_count
        self.current = 0

    def __iter__(self):
        return self

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

# Usage
for num in Counter(5):
    print(num, end=" ")  # 1 2 3 4 5

Separating Iterable and Iterator

Separate them to create reusable iterables.

class Range:
    """Reusable range"""

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

    def __iter__(self):
        return RangeIterator(self.start, self.end)

class RangeIterator:
    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
        value = self.current
        self.current += 1
        return value

# Can iterate multiple times
r = Range(1, 4)
print(list(r))  # [1, 2, 3]
print(list(r))  # [1, 2, 3] (can be used again)

3. Generator Functions

Using the yield keyword makes it easy to create iterators.

Basic Syntax

def count_up_to(max_count):
    count = 1
    while count <= max_count:
        yield count  # Return value and pause
        count += 1

# Usage
for num in count_up_to(5):
    print(num, end=" ")  # 1 2 3 4 5

# Or
gen = count_up_to(3)
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

How It Works

count_up_to(3) called
    │
    ▼
┌─────────────────┐
│  count = 1      │
│  while True:    │
│    yield 1 ─────┼──▶ Return, pause
│                 │
│  (next called)  │
│    count = 2    │
│    yield 2 ─────┼──▶ Return, pause
│                 │
│  (next called)  │
│    count = 3    │
│    yield 3 ─────┼──▶ Return, pause
│                 │
│  (next called)  │
│    while exits  │
│  StopIteration  │
└─────────────────┘

Multiple yield Values

def multi_yield():
    yield "First"
    yield "Second"
    yield "Third"

for value in multi_yield():
    print(value)

4. Generator Expressions

Similar to list comprehensions but use parentheses.

# List comprehension - stores everything in memory
squares_list = [x**2 for x in range(10)]

# Generator expression - generates on demand
squares_gen = (x**2 for x in range(10))

print(type(squares_list))  # <class 'list'>
print(type(squares_gen))   # <class 'generator'>

# Generators can only be iterated once
print(list(squares_gen))  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares_gen))  # [] (already exhausted)

Memory Efficiency

import sys

# List: uses full memory
list_comp = [x for x in range(1000000)]
print(sys.getsizeof(list_comp))  # ~8MB

# Generator: minimal memory
gen_exp = (x for x in range(1000000))
print(sys.getsizeof(gen_exp))    # ~200 bytes

5. yield from

Delegates values from another iterable.

def chain(*iterables):
    for it in iterables:
        yield from it  # Same as: for item in it: yield item

result = list(chain([1, 2], [3, 4], [5, 6]))
print(result)  # [1, 2, 3, 4, 5, 6]

Recursive Generator

def flatten(nested):
    """Flatten nested list"""
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

nested = [1, [2, 3, [4, 5]], 6, [7]]
print(list(flatten(nested)))  # [1, 2, 3, 4, 5, 6, 7]

6. Advanced Generator Features

send() - Send Values

You can send values to generators.

def accumulator():
    total = 0
    while True:
        value = yield total
        if value is not None:
            total += value

gen = accumulator()
print(next(gen))      # 0 (initialize)
print(gen.send(10))   # 10
print(gen.send(20))   # 30
print(gen.send(5))    # 35

throw() - Send Exceptions

def generator():
    try:
        yield 1
        yield 2
        yield 3
    except ValueError as e:
        yield f"Exception handled: {e}"

gen = generator()
print(next(gen))              # 1
print(gen.throw(ValueError, "Test"))  # Exception handled: Test

close() - Terminate Generator

def generator():
    try:
        yield 1
        yield 2
        yield 3
    finally:
        print("Cleanup")

gen = generator()
print(next(gen))  # 1
gen.close()       # Outputs "Cleanup"

7. itertools Module

Provides efficient iterator tools.

Infinite Iterators

from itertools import count, cycle, repeat

# count: infinite counter
for i in count(10, 2):  # Start at 10, increment by 2
    if i > 20:
        break
    print(i, end=" ")  # 10 12 14 16 18 20

# cycle: infinite repetition
colors = cycle(["red", "blue", "green"])
for _ in range(5):
    print(next(colors), end=" ")  # red blue green red blue

# repeat: repetition
for item in repeat("Hello", 3):
    print(item)  # Hello Hello Hello

Combinatoric Iterators

from itertools import chain, zip_longest, product, permutations, combinations

# chain: connect multiple iterables
print(list(chain([1, 2], [3, 4])))  # [1, 2, 3, 4]

# zip_longest: zip iterables of different lengths
a = [1, 2, 3]
b = ["a", "b"]
print(list(zip_longest(a, b, fillvalue="-")))
# [(1, 'a'), (2, 'b'), (3, '-')]

# product: Cartesian product
print(list(product("AB", [1, 2])))
# [('A', 1), ('A', 2), ('B', 1), ('B', 2)]

# permutations: permutations
print(list(permutations("ABC", 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

# combinations: combinations
print(list(combinations("ABC", 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'C')]

Filtering Iterators

from itertools import takewhile, dropwhile, filterfalse, compress

numbers = [1, 3, 5, 2, 4, 6]

# takewhile: while condition is true
print(list(takewhile(lambda x: x < 5, numbers)))  # [1, 3]

# dropwhile: skip while condition is true
print(list(dropwhile(lambda x: x < 5, numbers)))  # [5, 2, 4, 6]

# filterfalse: only items where condition is false
print(list(filterfalse(lambda x: x % 2, numbers)))  # [2, 4, 6]

# compress: filter by selector
data = ["A", "B", "C", "D"]
selectors = [1, 0, 1, 0]
print(list(compress(data, selectors)))  # ['A', 'C']

Grouping

from itertools import groupby

data = [
    {"name": "Alice", "dept": "HR"},
    {"name": "Bob", "dept": "IT"},
    {"name": "Charlie", "dept": "HR"},
    {"name": "David", "dept": "IT"},
]

# Must sort first!
data.sort(key=lambda x: x["dept"])

for dept, group in groupby(data, key=lambda x: x["dept"]):
    print(f"{dept}: {[p['name'] for p in group]}")
# HR: ['Alice', 'Charlie']
# IT: ['Bob', 'David']

Slicing

from itertools import islice

# Extract portion from infinite iterator
from itertools import count

first_10 = list(islice(count(1), 10))
print(first_10)  # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Specify start, stop, step
result = list(islice(range(100), 10, 20, 2))
print(result)  # [10, 12, 14, 16, 18]

8. Lazy Evaluation

Generators don't pre-compute values but generate them when needed.

Large File Processing

def read_large_file(filepath):
    """Generator that reads line by line"""
    with open(filepath, "r") as f:
        for line in f:
            yield line.strip()

# Memory-efficient processing
for line in read_large_file("huge_file.txt"):
    if "ERROR" in line:
        print(line)

Pipeline Processing

def numbers():
    for i in range(1, 1000001):
        yield i

def even_only(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def squared(nums):
    for n in nums:
        yield n ** 2

def less_than(nums, limit):
    for n in nums:
        if n >= limit:
            break
        yield n

# Pipeline: memory efficient
pipeline = less_than(squared(even_only(numbers())), 100)
print(list(pipeline))  # [4, 16, 36, 64]

9. Infinite Sequences

Fibonacci Sequence

def fibonacci():
    """Infinite Fibonacci sequence"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# First 10
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Prime Generator

def primes():
    """Infinite prime generation"""
    yield 2
    candidate = 3
    found = [2]
    while True:
        if all(candidate % p != 0 for p in found):
            found.append(candidate)
            yield candidate
        candidate += 2

# First 10 primes
from itertools import islice
print(list(islice(primes(), 10)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

10. Summary

Concept Description
Iterable Implements __iter__, usable in for loops
Iterator Implements __iter__ + __next__
Generator Function using yield
Generator Expression (x for x in iterable)
yield from Delegate to another iterable
send() Send value to generator
Lazy Evaluation Generate values when needed

11. Practice Problems

Exercise 1: Chunk Division

Create a generator that divides a list into chunks of specified size.

# chunk([1,2,3,4,5], 2) → [1,2], [3,4], [5]

Exercise 2: Sliding Window

Create a generator that produces sliding windows.

# sliding_window([1,2,3,4,5], 3) → (1,2,3), (2,3,4), (3,4,5)

Exercise 3: Tree Traversal

Create a generator that traverses a binary tree.


Next Steps

Check out 05_Closures_and_Scope.md to learn about variable scope and closures!

to navigate between lessons