Generators in Python

Generators in Python

here is a great deal of work in building an iterator in Python. We need to implement a class with __iter__() and __next__() method, monitor internal states and raise StopIteration when there are no values to be returned.

This is both extensive and unreasonable. The generator acts as the hero in such circumstances.

Python generators are a basic method of making iterators. All the work we referenced above is consequently taken care of by generators in Python.

Essentially, a generator is a function that returns an object (iterator) which we can iterate over.

Generator functions

Imagine that to solve some problem, you have to get the initial few multiples of some number x (for instance, the initial 4 products of 5 will be 5, 10, 15, 20, and so forth.). The clearest approach to do so is presumably to define a function multiples(x, num) as follows:

def multiples(x, num):
    i = 1
    result = []
    while i <= num:
        result.append(x * i)
        i += 1
    return result
print(multiples(13, 5))
 
print(multiples(5, 7))

Along these lines, multiples(x, num) gathers the first num products of a together in a list that is then returned. What are the disadvantages of such an approach?

All things considered, imagine that num is extremely huge. If you get all the values at once, you should keep an exceptionally enormous list in memory. Is it important? It depends, however certainly not if you are going to utilize each value only a single time. Or on the other hand, perhaps you don’t know precisely what number of multiples you will require, you simply should have the option to get them individually till some event happens.

For cases this way, generator functions are extremely useful. A custom generator can be declared similarly as an ordinary function with a single difference: the return keyword gets replaced with yield.

def multiples(x, num):
    i = 1
    while i <= num:
        yield x*i
        i += 1

At the point when a normal function is called, Python returns to its definition, runs the relating code with the provided argument values and returns the whole result with the return keyword to where the function is called from.

Generator functions, in turn, produce values one at a time, just when they are explicitly asked for another one, as opposed to giving them at the same time. Calling a generator doesn’t quickly execute it. Rather, a generator object is returned that can be iterated over:

multiples(13, 5)

Output

So as to get the generator function to really compute its values, we have to explicitly request the next value by passing the generator into the next() function. Note that yield really saves the condition of the function, so each time we request that the generator produce another value, execution proceeds from where it stopped, with a similar variable value it had before yielding.

multiples_of_num = multiples(13,5)

print(next(multiples_of_num))

print(next(multiples_of_num))

print(next(multiples_of_num))

print(next(multiples_of_num))

print(next(multiples_of_num))

print(next(multiples_of_num))

Output

Advantages of using generators

  • Without Generators in Python, delivering iterables is incredibly troublesome and extensive.
  • Generators simple to execute as they consequently implement __iter__(), __next__() and StopIteration which in any case, should be explicitly specified.
  • Memory is saved as the items are delivered as when required, not at all like ordinary Python functions. This reality turns out to be significant when you have to make an enormous number of iterators. This is additionally considered as the biggest advantage of generators.
  • Can be utilized to deliver an infinite number of items.
  • They can likewise be utilized to pipeline various operations.

Differences between Generator function and Normal function

Let’s see how Generator function differs from Normal function

  • Generator functions contain at least one yield statement.
  • When called, it returns an object (iterator) however doesn’t begin execution right away.
  • Methods like __iter__() and __next__() are executed automatically. So we can repeat through the items utilizing next().
  • When the function yields, the function is paused and the control is moved to the caller.
  • Local variables and their states are recollected between progressive calls.
  • At last, when the function terminates, StopIteration is raised automatically on further calls.

Let’s understand the yield and next functions with the help of an example:

def func():
    n = 1
    print('This is first')
    
    yield n

    n += 1
    print('This is second')
    yield n

    n += 1
    print('This is at last')
    yield n

Now, lets run the program using the following statements

a = func()
next(a)
next(a)
next(a)
next(a)
next(a)

Output

Generator expressions

Another method of defining a generator will be generator expressions, which are like list comprehensions. The main difference in the syntax are the brackets: one should utilize square brackets [] for list comprehension statements and the round ones () for characterizing a generator. Analyze:

numb = [10, 20, 30, 40]
 
my_gen = (n ** 2 for n in numb)
  
print(next(my_gen))
 
print(next(my_gen))
 
print(next(my_gen))

my_list = [n ** 2 for n in numb]  
 
print(my_list)

Output

Generator expressions are very convenient to use in a for loop. A new value is automatically generated at each iteration:

my_gen = (n ** 2 for n in numb)
 
for n in my_gen:
    print(n)

Output

Why are generators useful?

Up until now, we’ve discovered that generators produce a single value from a defined sequence just when they are explicitly approached to do as such. This methodology is called lazy evaluation.

Lazy evaluation makes the code considerably more memory efficient. In reality, at each point in time, just values are produced and stored in memory individually: the previous value is overlooked after we have moved to the following one and, therefore, doesn’t occupy space.

Remember, in any case, that exactly the previous value is overlooked when the new one should be generated, we can just go over the values once.

Conclusion

Let’s sum it up:

  • Generators allow one to declare a function that behaves like an iterator.
  • Generators are lazy because they only give us a new value when we ask for it.
  • There are two ways to create generators: generator functions and generator expressions.
  • Generators are memory-efficient since they only require memory for the one value they yield.
  • Generators can only be used once.

Now, let’s try to solve a problem

Imagine you are writing a program that will give a prediction for users. Say, you intend to do so based on a number they enter. Write a program that reads a number and prints the sum of its digits.

An input number and hence the output number are both positive.

Sample Input 1:

309
Sample Output 1:

12
Sample Input 2:

10931
Sample Output 2:

14

numbers = list(input())
numbers = list(map(int, numbers))
print(sum(numbers))

Output