Generators
Generators are functions and a function is deemed as a Generator function if it has one or more yield statements. Generators also provide us with a very simplistic way of creating iterator objects. They also do not store all of the values instead they generate one value at a time.
In a practical setting, this becomes useful in areas of Data Science, where we have thousands or even millions of user data to work with. Once we process it, it can generate a large data set and so instead of receiving it altogether, we can work with one data at a time.
Before we further explore the concept of generators let us first understand the use of the ‘yield’ keyword as well as the concept of iterator objects.
Yield & Iterator Objects
The ‘yield’ keyword is similar to the ‘return’ keyword in that they give us a value but they differ in that, unlike return, yield saves the value of the local variable. Return ends a function whereas Yield simply pauses it and when it returns instead of resetting, it picks up where it left off.
To access a value from ‘yield’ we first need to create an iterator object and then iterate through it with a function called next() or with a for loop. An Iterator is present all over Python, and to put it simply, it is an object that will return one data at a time and an Iterable are objects from which we can get an iterator. Examples of Iterable objects are strings, lists, tuples, and so on.
Consider an example:
def counter_yield(): temp_no = 100 print("First item to be printed:") yield temp_no # The function now pauses and the control goes back to the caller # When called again it resumes from here temp_no += 100 print("Second item to be printed:") yield temp_no # Now StopIteration is raised # Any calls after this will raise StopIteration # Create an iterable object object_1 = counter_yield() # Iterating through the object using next print(next(object_1)) print(next(object_1))
The output is:
As we can see the value of “temp_no” is saved and remembered such that the next time we call it, it continues from where it left off. Whereas in a normal function the local variable would have been destroyed.
Also if we try to call the next function again it will result in an error:
print(next(object_1)) # This will raise an error
Closures
A closure is a nested function that can store as well as have access to variables within the local scope in which it was formed even after the outer function has completed execution. A closure behaves like a function that works in the same way as an object of a class, this means that we can call this function object later on to access different variables and parameters of our outer function.
def welcome_guest(number): # Variable of outer function number_of_guests = number print(f"{number_of_guests} guests came to our house") def inside_house(): print(f"We served {number_of_guests} drinks to all of the guests") return inside_house # prints outer function statement once function_object_1 = welcome_guest(5) # prints inner function statement twice function_object_1() function_object_1()
This should give us:
Here we created an outer function called “welcome_guest” and an inner function called “inside_house”. Both of these have a print statement, and because of this, we see the way the function behaves like a class.
If we had some code within a constructor, each time we created an object of the class it would run all of the code within the constructor, thus since we created an object of the function once, it will display the code from the outer function once but since we called the object twice it displays the code from within the inner function twice.
Our object can run the inner function because the outer function returns the inner function, using the return statement. When we return an inner function to an object, the object is directly referring to the inner function. Let’s print out our object:
function_object_1 = welcome_guest(5) print(function_object_1)
We get:
Let’s add some parameters to the inner function to better understand how these work let’s add a parameter to our inner function. Modify the inner function as shown below:
def inside_house(drink_served): print(f"We served {number_of_guests} {drink_served} to all of the guests")
Now let’s call the objects of this inner function with parameters.
function_object_1("Lemonades") function_object_1("Soft drinks")
We get:
What have we learned?
- What is a Generator?
- What is the use of the yield keyword?
- What is the similarity and difference between yield and return?
- What are iterators and iterables?
- What is a Closure?