Generators & Clousers

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?
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments