Learn Generators in Python with (Examples)
Tags: programming, python, beginners, generators
Published: 2021-11-12 17:47:28

In this Article we will going to look following topics

* Understanding **Generators** in Python,with Examples
* Also how to construct our own *Generator* function 
* Advantages of using *Generator* function instead of *regular* function in some cases


### Generators :-
	
* *Generator* are the special function which returns an traversal object which used to iterate over a *collection of values*, it is also used to create *iterators*.
	
* It is can also be expressed in a way which is simiar to *list compreshension*

* Just like iterator *next()* method we can also iterate over *generator* using *generator.next()* to iterate over the *generator*.
	
#### Creating a Geneartor :-
* It is simple to create a *Generator* function if you have worked with regular function.

* In *Generator* function instead of using *return* statement we use yield statement.
* A function with *yield* keyword in it is called a *Generator* function

* *generator* throws *StopIteration* Exception when *next()* method called on the last *element* in the genearator,

* We have to *explicitly* handle *StopIteration* Exception when
	  using *next()* method with *generator*
	  
	
let's use take an example to understand the above points :-

### Example :-	
```
# Normal function
def func():
    letters = ['a', 'b', 'c', 'd', 'e']
    for c in letters:
        return c

# Generator function
def generator_func():
    letters = ['a', 'b', 'c', 'd', 'e']
    for c in letters:
        yield c

print("Normal function :-")
print(func())

print("\nGenerator function :-")
print(generator_func())

print("\nIterating over generator using next() method")
gen = generator_func()
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))

Output :-

Normal function :-
a

Generator function :-
<generator object generator_func at 0x00000180318F9A10>

Iterating over generator using next() method
a
b
c
d
e
Traceback (most recent call last):
  File "C:\Users\adars\Desktop\test.py", line 26, in <module>
    print(next(gen))
StopIteration	
```
Now , you can se a normal function return the value which we have return but , *Generator* function return *generator* object (traversal object)

**we can *iterate* over the *generator object*, let's see how?**

### Example :-
```
for i in generator_func():
    print(i)
	
Output :-
a
b
c
d
e
```

we iterated over the generator object in the above code,

Now you're probably thinking what is the **difference** between :-
* *function with return*
* *function with yield*


#### **function with *return* statement**
* when the *return* statement called , the function terminates it execution
  and return the value, removing the function from the *call stack*.

#### **function with *yield* statement**
* *yield* controls the flow of *Genearator*
* *yield* pauses the function state and  *yielded* value to the caller
* *yield* resume the function execution on *yield* statement successfully
* there can be multiple *yield* statement per *Generator* function


### Generator with multiple *yield* statements

### Example :-
```
def generator_func2():
    numbers = [1, 2, '3', '4', 5, 6, '7', 8, 9, '10']
    for num in numbers:
        if type(num) == int:
            yield f"{num} is an integer form"
        elif type(num) == str:
            yield f"{num} is an string form"
            
            
            
for i in generator_func2():
    print(i)
            

Output :-
1 is an integer form
2 is an integer form
3 is an string form
4 is an string form
5 is an integer form
6 is an integer form
7 is an string form
8 is an integer form
9 is an integer form
10 is an string form
```

In the above example we created a *Generator* function which uses
multiple *yield* statments


#### Creating *Generator* in one line
* Just like *list comprehension* , we can create *Generators* in single line
* Instead of *[ ]* brackets ,we use *( )* to create a *generator* function

### Example :-
```
numbers_generator = (i for i in range(10))
print(type(numbers_generator))

for num in numbers_generator:
    print(num)
	
Output :-
0
1
2
3
4
5
6
7
8
9
```
In the above example we have created a *Generator* function  which belong to *generator* class, in a way similar to *list comprehension*

**let's take some more example of *generators* function**

**If your fimilar with *range()* function in python , we will going to implement our own *range()* function with the help of *generators*.**

### Example :-
```
def range(start = 0,stop = 0,stepby = 1):
    yield start
    while(start < stop):
        yield start+stepby
        start += stepby

print("range() function implementation :-")
print(type(range(0,10)))


for i in range(0,10):
    print(i)
    
print("\n---------\n")

for i in range(0,10,2):
    print(i)

Output :-
range() function implementation :-
<class 'generator'>
0
1
2
3
4
5
6
7
8
9
10

---------

0
2
4
6
8
10
```

We have created a simple *implementation* of *range()* function 
using *generators*

**We will going to write two implementation of  *fibonacci sequence* using *generators*

###Example
```
def fibo_seq_generator(n):
      
    # Initialize first two Fibonacci Numbers 
    a, b = 0, 1
  
    # One by one yield next Fibonacci Number
    while a < n:
        yield a
        a, b = b, a + b
        
print("Fibonacci sequence using generator :- ")
for i in fibo_seq_generator(20):
    print(i)
	
Output :- 
Fibonacci sequence using generator :-
0
1
1
2
3
5
8
13
```

Now that we have covered many examples of *generators* and
learned how to create *generators*, now we're going to see 
the advantages of generator :-

### Advantages of **Genearators**

* It is Easy to Implement a *generators* relatively compared to
an iterator , which requires you to implement **_iter__()** and
 **__next__()** function.
 

* *Genearators* are memory efficient when working with large
   sequence of numbers because *yield* statement give value to
   the caller function whenever *yield* line gets executed ,
   So it doesn't have to store the entire sequence in the memory,
   whereas in case of normal function ,it stores the sequence
   somewhere in memory(in call stack) before returning the result.


* With *Generators* we can generate infinite sequence of numbers without needing to worry about computer memory usage.
  

*  *Generators* used in **Data Pipeline** which provides the facility to process large datasets or stream of data without using extra computer memory.   
   

with this we have successfully completed
*Generators in Python with (Examples) Article* 
hope you all have understand the topics
covered in this Article.

:-)