Code-Memo

Iterators

Iterators are objects that implement the iterator protocol, consisting of two methods:

  1. __iter__(): Returns the iterator object itself.
  2. __next__(): Returns the next item in the sequence. When no more items are available, it raises the StopIteration exception.

In Python, any object that implements these methods can be used as an iterator. Iterators are used in for loops, comprehensions, and other contexts where sequential or lazy evaluation is required.

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

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

Generators

Generators are a type of iterable, but unlike regular classes implementing the iterator protocol, generators use the yield statement to produce a series of values lazily. This means they generate values only when requested and maintain their state between calls.

Key characteristics of generators:

Types of Generators

  1. Generator Functions: Defined using the def keyword with yield statements inside the function body.
def square_numbers(n):
    for i in range(n):
        yield i ** 2

# Usage:
gen = square_numbers(5)
for num in gen:
    print(num)
  1. Generator Expressions: Similar to list comprehensions but using round brackets () instead of square brackets [].
gen_exp = (x ** 2 for x in range(5))
for num in gen_exp:
    print(num)

Advantages of Generators

Advanced Concepts

Using send():

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

acc = accumulator()
next(acc)  # Prime the generator
print(acc.send(1))  # Output: 1
print(acc.send(2))  # Output: 3
acc.close()