Going Beyond Lists: An Introduction to Iterators and Generators in Python

· 9 min read December 7, 2022

banner

Iterators and generators are powerful tools in Python that can help you improve the efficiency and performance of your code. In this article, we’ll take a deep dive into these important concepts, covering everything from the basics of iterators and generators, to advanced techniques for using them in your Python programs.

We’ll also look at how to use iterators and generators to handle large data sets, and how to control the execution of a generator using the yield and send keywords. Whether you’re a beginner looking to learn about these concepts for the first time, or an experienced Python developer looking to deepen your understanding, this article has something for you. Let’s get started!

What are Iterables

An iterable is an object that represents a sequence of elements that can be iterated over . This means that the object can be used in a for loop, or in other places where a sequence is needed, such as the zip() and map() functions. Examples of iterables include all sequence types, such as lists, strings, and tuples, as well as some non-sequence types like dictionaries and file objects.

To create an iterable,an object must implement the __iter__() method, which returns an iterator for the object. An iterator is an object that maintains the iteration state and provides access to the elements in the sequence. In Python, an iterator is an object that implements the iterator protocol, which consists of the __iter__() and __next__() methods.

When an iterable object is passed as an argument to the iter() function, it returns an iterator for the object. This iterator can then be used to iterate over the elements in the sequence. In most cases, however, it is not necessary to call iter() or deal with iterator objects directly. The for statement automatically creates a temporary iterator for the duration of the loop, allowing you to iterate over the elements in the sequence without having to explicitly create an iterator.

In summary, an iterable is an object that represents a sequence of elements that can be iterated over, and an iterator is an object that maintains the iteration state and provides access to the elements in the sequence. Together, these two concepts form the foundation of many powerful features in Python, such as for loops and the ability to iterate over large data sets without using a lot of memory.

What Are Iterators

In Python, an iterator is an object that represents a stream of data, and can be used to iterate over an iterable. In other words, an iterator is an object that can be used in a for loop to loop over an iterable.

An iterator has two important methods that allow it to be used in a for loop: __iter__() and __next__(). The __iter__() method returns the iterator object itself, allowing the iterator to be used in a for loop. The __next__() method returns the next element in the iterator, moving the iterator to the next position in the iterable.

As an iterator also implements the __iter__ method it is also a iterable

Here’s an example of how to create an iterator in Python:


# Create a list of numbers
numbers = [1, 2, 3, 4, 5]

# Get an iterator object from the numbers list
iterator = iter(numbers)

# Loop through the iterator and print the numbers
for number in iterator:
    print(number)

In this code, we create a list of numbers called numbers and then get an iterator object from that list using the iter() function. We then loop over the iterator object using a for loop, which will print each number in the list to the screen.

Iterators are exhaustible and can only be used once . When an iterator is exhausted, it means that it has no more elements to iterate over. This can happen in a few different ways, depending on the specific iterator and the context in which it is being used.

For example, if you are using a for loop to iterate over the elements of an iterator, the loop will terminate when the iterator is exhausted. In this case, exhaustion of the iterator occurs when the iterator’s __next__() method raises a StopIteration exception, indicating that there are no more elements to return.

Another way that an iterator can be exhausted is if it is a finite iterator that has reached the end of its sequence. In this case, the iterator will no longer be able to return any more elements, and will be considered exhausted.

Exhaustion of an iterator is not always an error, and in many cases it is a natural and expected part of the iteration process. However, it is important to be aware of when an iterator is exhausted, and to handle this situation properly in your code.

What are generators

A generator is a special type of iterator that generates values on the fly, rather than storing them in memory. This makes generators much more memory-efficient than lists or other data structures, as they only hold one value at a time. To create a generator, the keyword yield is used instead of return in a function. When a generator function is called, it returns a generator object that can be iterated over in a for loop.

Here’s an example of a generator function that generates the squares of numbers from 0 to 2:

def create_generator():
    for i in range(3):
        yield i * i

This function uses the yield keyword to return the square of each number in the range from 0 to 2. To create a generator object from this function, we can call it and assign the returned value to a variable:


mygenerator = create_generator()

#We can then iterate over the generator object using a for loop:

for i in mygenerator:
    print(i)

This will print the squares of the numbers 0, 1, and 2 to the screen: 0, 1, and 4.

One important thing to note about generators is that like iterators they can only be used once.

More Generator Examples

let’s look at a more detailed example of how to use generators in Python. Suppose we have a list of numbers, and we want to generate a new list containing only the even numbers from the original list. We could do this using a traditional for loop, like this:


# Original list of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# List to hold the even numbers
even_numbers = []

# Loop through the numbers and append the even ones to the even_numbers list
for number in numbers:
    if number % 2 == 0:
        even_numbers.append(number)

# Print the even numbers
print(even_numbers)

This code will create a new list called even_numbers and loop through the numbers in the original list. If a number is even, it will be appended to the even_numbers list. At the end, the even_numbers list will be printed to the screen.

However, this approach has one major drawback: it stores all of the numbers in the even_numbers list in memory. If the original list of numbers is very large, this can cause memory issues and slow down your program.

To avoid this problem, we can use a generator instead. A generator function will only generate the even numbers on the fly, rather than storing them in memory. Here’s how we could do this:


# Original list of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Generator function that yields only the even numbers from the list
def even_numbers(numbers):
    for number in numbers:
        if number % 2 == 0:
            yield number

# Create a generator object from the even_numbers generator function
generator = even_numbers(numbers)

# Loop through the generator object and print the even numbers
for number in generator:
    print(number)

This code works similarly to the previous example, but instead of storing the even numbers in a list, it uses a generator function to yield each even number as it is encountered. The generator function is called even_numbers, and it takes a list of numbers as an argument. Inside the function, we loop through the numbers and use the yield keyword to return each even number as it is encountered.

To create another generator object from the even_numbers function, we simply call the function and assign the returned value to a variable

Generator Comprehensions

A generator comprehension is a concise way to create a generator in Python. It is similar to a list comprehension, but instead of creating a list, it creates a generator.

A generator comprehension consists of a bracketed expression followed by a for clause, and can include optional if and for clauses. The result of a generator comprehension is a generator that produces the elements in the sequence defined by the comprehension.

Here is an example of a generator comprehension:


# Create a generator that produces the squares of the numbers from 1 to 10
squares = (x**2 for x in range(1, 11))

# Loop through the generator and print the squares
for square in squares:
    print(square)

In this code, the generator comprehension (x**2 for x in range(1, 11)) creates a generator that produces the squares of the numbers from 1 to 10. We then loop over the generator using a for loop, which will print each square to the screen.

Generator comprehensions are a convenient way to create generators, and can help you write more concise and efficient code.

Advanced Generators Methods

In addition to being a memory-efficient way to iterate over sequences of data, generators in Python have some other useful features that make them a powerful tool for many applications.

One such feature is the ability to pause and resume a generator. When a generator function encounters the yield keyword, it will pause execution and return the yielded value. When the generator is called again (using builtin next or __next__ method ), it will resume execution from the point where it left off, rather than starting from the beginning of the function.

This allows you to write generator functions that can be paused and resumed as needed, which can be useful in many situations. For example, you could write a generator function that yields a series of numbers, and then use that generator to compute the sum of those numbers:

This may be lame for real life but makes a good example . In real life you can have some complex logic . like the pytest library takes advantage of generators to power its fixtures


# Generator function that yields a series of numbers

def numbers():
yield 1
yield 2
yield 3

# Create a generator object from the numbers generator function

generator = numbers()

# Use the generator to compute the sum of the numbers

sum = 0
for number in generator:
sum += number

# Print the sum

print(sum)

In this code, the numbers generator function yields the numbers 1, 2, and 3. When the for loop begins, the generator will start execution at the beginning of the function and yield the first number, 1. The sum variable will be updated to include this number, and then the generator will pause execution at the yield keyword.

When the for loop calls the generator again, it will resume execution from the point where it left off, yielding the next number in the sequence: 2. This process will continue until all of the numbers have been yielded, at which point the for loop will end and the final value of the sum variable will be printed to the screen.

Another useful feature of generators is the ability to pass values into a generator using the send() method. This allows you to control the execution of a generator from outside the generator function, by sending values into the generator and receiving values that are yielded by the generator.

Here’s an example of how to use the send() method with a generator:



# Generator function that yields numbers and receives input

def numbers(): # Yield the first number
    number = yield 1 # Receive the next number and yield it
    number = yield number + 1 # Receive the next number and yield it
    yield number + 1

# Create a generator object from the numbers generator function

generator = numbers()

# Send a value into the generator and print the yielded value

print(generator.send(None)) # Prints 1

# Send a value into the generator and print the yielded value

print(generator.send(5)) # Prints 6

# Send a value into the generator and print the yielded value

print(generator.send(7)) # Prints 8

In this code, the numbers generator function yields a series of numbers. The first time the generator is called, it yields the value 1. When the generator is called again, it receives a value (in this case, 5) and yields the value 6. When the generator is called again, it receives a value (in this case, 7) and yields the value 8.

This allows you to control the execution of the generator from outside the generator function, by sending values into the generator and receiving values that are yielded by the generator.

That should be it for the topic , hope it was worth a read . I’m always looking for new ways to improve and grow, so please feel free to reach out with any feedback or suggestions.

Some Thing to say ?