6 Examples to Master Python Generators | by Soner Yıldırım | Towards Data Science

The generators in Python are one of those tools that we frequently use but do not talk about much. For instance, most for loops are accompanied with the range function which is a generator.
Generators allow for generating a sequence of values over time. The main advantage of using a generator is that we do not have to create the entire sequence at once and allocate memory. Instead, the generator returns one value at a time and waits until the next value is called.
In this article, we will go over 6 examples to demonstrate how generators are used in Python as well as some tips to keep in mind.
Let’s start with a simple yet frequently used generator. The range function is used for iterating over a range of values determined by the given start, stop, and step size.
range(5)out> range(0, 5)for i in range(5):   print(i)01234
If you execute the range function, you will not get any value returned. However, we can iterate over it in a for loop to access the values in order. To provide the flow of numbers in a sequence, the range function keeps track of the last number and the step size.
We can also view the entire sequence by converting a generator to a list.
list(range(5))[0, 1, 2, 3, 4]
We can create our own generator functions. The syntax is quite similar to creating a normal function but there is a critical difference.
The following is a generator function that returns the cube of every other number in a given range starting from 1.
def mygenerator(n):   for i in range(1, n, 2):      yield i**3
The main difference between a generator and a normal function is that we do not use the return keyword. Instead, the yield keyword is used.
We can now use our generator in a for loop:
for i in mygenerator(10):   print(i)1 27 125 343 729
Another way to create a generator is by using a generator expression. It is similar to a list comprehension.
mygenerator = (i**3 for i in range(1,10,2))mygenerator<generator object <genexpr> at 0x7f867be7b9d0>
We can now iterate over it.
for i in mygenerator:   print(i)1 27 125 343 729
It is important to note that once we iterate over a generator and reach the end, we can not iterate over it again. For example, if we rerun the for loop above, it will not return anything.
This is not the case in example 2 because we had a generator function which creates a generator each time it is executed. You may notice that we do not have a function call in this example. Instead, we have a generator object.
We can iterate over a generator manually by using the next function. Let’s first create a new generator object.
def mygenerator(n):   for i in range(1, n, 2):      yield i * (i + 1)my_gen = mygenerator(6)
We can now use the next function to ask for the next value that the generator will return.
next(my_gen)2next(my_gen)12next(my_gen)30next(my_gen)> StopIteration error
If we call the next function on the generator after we reach the end, it will return a StopIteration error. The for loop does the same steps as we do with the next function. However, we do not get a StopIteration error when using a for loop because it automatically catches this error.
We often see the terms iterable and iterator. An iterable is an object that we can iterate over such as lists, sets, strings, and so on. However, we cannot take iterable objects as iterators.
There is a built-in Python function that converts an iterable to an iterator. Surprisingly, it is the iter function. For instance, we can iterate over strings but cannot use them as iterators. The iter function allows using strings as iterators.
my_string = "Python"next(my_string)> TypeError: 'str' object is not an iterator
Let’s convert it to an iterator and then call the next function again.
my_string_new = iter(my_string)next(my_string_new)'P'next(my_string_new)'y'
The iter function can also be used to convert a list to an iterator.
my_list = ['a', 'b', 'c', 'd', 'e']my_gen = iter(my_list)type(my_gen)list_iterator
Let’s use it in a for loop.
for i in my_gen:   print(i)abcde
Generators are pretty useful when working with sequences that allocate a large amount of memory. Generators support an iteration protocol so when we call them, they do not return a value and exit.
After the next value in the sequence is generated, they automatically suspend and resume their execution state at the last point when the next value is asked for.
The advantage is that instead of having to compute the entire sequence at once, generators compute one value at a time and wait until the next value is called for.
Thank you for reading. Please let me know if you have any feedback.