Unit-3 Functional Programming
Unit-3 Functional Programming
A. PROGRAMMING PARADIGMS:
A programming paradigm is a general approach to developing software. There aren’t
usually fixed rules about what is or isn’t part of a particular paradigm, but, rather, there
are certain patterns, characteristics, and models that tend to be used. This is especially
true of Python since it supports several paradigms with no real dividing line between
them. Here are the paradigms available in Python.
1. PROCEDURAL PROGRAMMING:
Procedural programming is the most basic form of coding. Code is structured
hierarchically into blocks (such as if statements, loops, and functions). It is arguably the
simplest form of coding. However, it can be difficult to write and maintain large and
complex software due to its lack of enforced structure.
2. OBJECT-ORIENTED PROGRAMMING:
Object-oriented programming (OOP) structures code into objects. An object typically
represents a real item in the program, such as a file or a window on the screen, and it
groups all the data and code associated with that item within a single software structure.
Software is structured according to the relationships and interactions between different
objects. Since objects are encapsulated, with well-defined behavior, and capable of being
tested independently, it is much easier to write complex systems using OOP.
3. FUNCTIONAL PROGRAMMING:
Functional programming (FP) uses functions as the main building blocks. Unlike
procedural programming, the functional paradigm treats functions as objects that can be
CHARACTERISTICS OF FUNCTIONAL
PROGRAMMING:
1. Functions as first class objects, which means that you should be able to apply all the constructs
of using data, to functions as well.
2. Pure functions; there should not be any side-effects in them.
3. Ways and constructs to limit the use of for loops
4. Good support for recursion
Using functions as first class objects means to use them in the same manner that you use
data. So, You can pass them as parameters like passing a function to another function as an
argument.
In [ ]:
print("Output: ",list(map(int,["1","2","3"])))
B. COMPREHENSIONS IN PYTHON
1. LIST
2. DICTIONARY
3. SET
LIST COMPREHENSION:
1. PYTHON SUPPORTS COMPUTED LISTS CALLED LIST COMPREHENSIONS.
2. SYNTAX
List =[expression for variable in sequence]
3. Where the expression is evaluated once, for every item in the sequence.
localhost:8888/nbconvert/html/UNIT-3 FUNCTIONAL PROGRAMMING.ipynb?download=false 2/27
6/8/22, 10:33 AM UNIT-3 FUNCTIONAL PROGRAMMING
In [ ]:
#Create a list of cubes of first 5 natural numbers
cubes=[]
for i in range(0,5):
cubes.append(i**3)
print ("Cubes of first 5 Natural Numbers are: ",cubes)
In [ ]:
#Using List Comprehensions:
1. WAP that creates a list of 10 random integers. Then create two list i.e, ODD LIST and EVEN LIST that has all the
values respectively.
In [ ]:
import random
number=[]
for i in range(10):
val=random.randint(1,100)
number.append(val)
even_L=[]
odd_L=[]
for i in range(len(number)):
if (number[i]) %2==0:
even_L.append(number[i])
else:
odd_L.append(number[i])
2. WAP to create a list of first 20 odd numbers using the list comprehension method
In [ ]:
odd=[2*i +1 for i in range(20)]
print(odd)
3. WAP to combine the value in two lists using list comprehension. Combine only those value which of a list that are
multiples of values in the first list.
In [ ]:
Result=([(x,y) for x in [10,20,30] for y in [35,40,55,60] if y%x==0 or x%y==0])
print(Result)
In [ ]:
letters = []
print(letters)
In [ ]:
c_letters = [ letter for letter in 'Computer Science' ]
print( c_letters)
In [ ]:
#Addition of Matrix
M1=[[1,2,3],[4,5,6]]
M2=[[4,5,6],[7,8,9]]
S=[]
for i in range(len(M1)):
TR=[]
for j in range(len(M1[0])):
TR.append(M1[i][j] + M2[i][j])
S.append(TR)
print("Sum of Matrix is: ",S)
#print(M1[1][2])
In [ ]:
M1=[[1,2,3],[4,5,6]]
M2=[[4,5,6],[7,8,9]]
S=[[M1[i][j] + M2[i][j] for j in range(len(M1[0]))] for i in range(len(M1))]
print(f" Sum of Matrix is: ",S)
In [ ]:
#Transpose of Matrix
#Using nested loop
M=[[1,2,3],[4,5,7]]
T=[]
for i in range(len(M[0])):
TR=[]
for row in M:
TR.append(row[i])
T.append(TR)
print(T)
In [ ]:
#Using list comprehension
8. WAP to encrypt the message in which each alphabet is replaced by characater having 3 character difference in the
alphabetic order.
In [ ]:
def convert(ch):
ch=ch.lower()
conv_char=chr(ord(ch)+3)
if conv_char in 'abcdefghijklmnopqrstuvwxyz':
return conv_char
elif ord(conv_char)>122 and ord(conv_char)<126:
return chr(ord(conv_char)-26)
else:
return ch
msg=input("Enter the Message: ")
encrypt="".join(map(convert,msg))
print("Encrypted Message is: ",encrypt)
DICTIONARY COMPREHENSIONS:
1. Dictionaries are data types in Python which allows us to store data in key/value pair.
2. Key: Value pairs is required to create a dictionary. To get these key-value pairs using dictionary
comprehension the general statement of dictionary comprehension is as follows:
{key: value for var in iterable}
3. Dictionary comprehension is an elegant and concise way to create dictionaries.
1. WAP to generate numbers as keys and their squares as values within the range of 10.
In [ ]:
square_dict = dict()
for num in range(1, 11):
square_dict[num] = num*num
print(square_dict)
In [ ]:
square_dict = {num: num*num for num in range(1, 11)}
print(square_dict)
In [ ]:
keys=[1,2,3,4]
values=['a','b','c','d']
d={key:value for key,value in zip(keys,values)}
print(d)
localhost:8888/nbconvert/html/UNIT-3 FUNCTIONAL PROGRAMMING.ipynb?download=false 5/27
6/8/22, 10:33 AM UNIT-3 FUNCTIONAL PROGRAMMING
ZIP FUNCTION:
zip() is a built-in function that takes two or more sequences and zips them into a list of
tuples.The tuple thus, formed has one element from each sequence.
In [ ]:
Tup=(1,2,3,4,5)
List1=['a','b','c','d','e']
print(list((zip(Tup,List1))))
SET COMPREHENSION:
1. Set comprehensions are pretty similar to list comprehensions.
2. The only difference between them is that set comprehensions use curly brackets { }.
In [ ]:
mylist = [1, 2, 3, 4, 4, 5, 6, 6, 6, 7, 7]
myset = set()
# Using loop for constructing myset
for var in mylist:
if var % 2 == 0:
myset.add(var)
print("Set using for loop:",myset)
In [ ]:
# Using Set comprehensions
mylist = [1, 2, 3, 4, 4, 5, 6, 6, 6, 7, 7]
myset = {var for var in mylist if var % 2 == 0}
print("Set using set comprehensions:", myset)
C. CLOSURE:
PRE-REQUISITE FOR UNDERSTANDING CLOSURE:
1. NESTED FUNCTION : A function defined inside another function is called a nested function. Nested functions can
access variables of the enclosing scope.
2. NON-LOCAL VARIABLE: Non-Local variables are read-only by default and we must declare them explicitly as non-
local (using nonlocal keyword) in order to modify them.
In [ ]:
def outer_func():
message = 'Hello'
def inner_func():
print(message)
return inner_func
NESTED FUNCTION:
1. In the inner_func, you're actually looking at :
In [9]:
#Example for non-local
def fun():
x = 5
print(x) # will print the value of x (5) and will not throw error
DEFINITION OF CLOSURE:
1. Closure in Python is an inner function object, a function that behaves like an object,
2. Closure remembers and has access to variables in the local scope in which it was created
even after the outer function has finished executing.
3. It can also be defined as a means of binding data to a function (linking / attaching the data with
the function so that they are together), without passing it as a parameter. Even if values in
enclosing scopes are not present in memory, a closure is a function object that
remembers those values.
1. It is a nested function.
2. It has access to a free variable in outer scope.
3. It is returned from the enclosing function.
4. Closure can be called a function object (as it is a function that behaves like an object) that
is capable of remembering values that are in enclosing scopes (such as the outer
functions) even if they are not present in memory.
NOTE: A free variable is a variable that is not bound in the local scope.
POINT TO PONDER: Python closure help avoiding the usage of global values and provide some
form of data hiding.
SUMMARY: A python closure isn't like a plain function. It allows the function to access those captured
variables through the closure’s copies of their values or references, even if the function is invoked
outside their scope.
In [ ]:
# Example : Nested function
# When a function is defined inside another function.
def outerfunc():
x = 10
def innerfunc():
print(x)
innerfunc()
outerfunc()
EXPLAINATION OF ABOVE CODE: inner() could read the varibale x which is non-local to it and it
must modify x we declare that it's non-local to inner().
In [ ]:
#Programme to elaborate Closure
def power(exponent):
def power_of(base):
return pow(base,exponent)
return power_of
square=power(2)
print(square(2))
print(square(3))
print(square(4))
print(square(5))
print(square(6))
cube=power(3)
print(cube(2))
In [6]:
#Closure
def divider(y):
def divide(x):
return x / y
return divide
d1=divider(3)
print(d1(9))
3.0
1. Sometimes you might have variables in the global scope that are not used by more than one
function. Instead of declaring the variables in global scope, you must think of using a closure.
You can define them in the outer function and use them in the inner function. Closures can
also be used to avoid the use of global scope.
1. Have you thought about calling the inner function directly at any point of time? You cannot do
it! The only way to access the inner function is by calling the outer function. Data hiding is an
D. ITERATOR IN PYTHON:
DEFINITION: An Iterable is an object that contains a sequence or countable values
that can be traversed upon. It is the object that is used to iterate over iterable
objects like lists, tuples, dicts, and sets.
ITERABLES: Iterables are objects that act as iterables containers to get the iterator.
Anything that you can loop over in Python is called an iterable. For an object to be
considered an iterable, it must have the iter() method.
ITERABLES IN PYTHON
ITERATOR PROTOCOL:
The iterator objects are required to support the following two methods, which together form the
iterator protocol:
1. iterator.iter(): Return the iterator object itself. This is required to allow both containers (also
called collections) and iterators to be used with the for and in statements.(PRESENT IN
ITERABLES)
2. iterator.next(): Return the next item from the container. If there are no more items, raise the
StopIteration exception.(PRESENT IN ITERATOR)
In [1]:
#Iterable 'A' directory containing __iter_() method
A=[1,2,3,4,5]
print(dir(A))
In [2]:
# Iterator 'value' directory containing __next__() method
A=[1,2,3,4,5,6,7]
value=A.__iter__()
print(dir(value))
SEQUENCES: LIST,STRINGS,TUPLES
ITERABLES: DICTIONARY,FILE,OBJECTS,SETS
In [ ]:
numbers=[1,2,3,4,5,6,7,8,9,10]
strings="Class of Computer Science 2021"
tuples=("Car","Motorcycle","Bus","Aeroplane","Tanks")
They support efficient element access using integer indices via the getitem() special method
(indexing) and define a length() method that returns the length of the sequence.
In [7]:
# Element access using integer indices
numbers=[1,2,3,4,5,6,7,8,9,10]
strings="Class of Computer Science 2021"
tuples=("Car","Motorcycle","Bus","Aeroplane","Tanks")
print(numbers[4])
print(strings[0])
print(tuples[-5])
5
C
Car
In [12]:
# Slicing the sequences
numbers=[1,2,3,4,5,6,7,8,9,10]
strings="Class of Computer Science 2021"
tuples=("Car","Motorcycle","Bus","Aeroplane","Tanks")
print(numbers[:2])
[1, 2]
of Computer Science 2021
('Bus', 'Aeroplane', 'Tanks')
index = 0
numbers = {1, 2, 3, 4, 5}
while index < len(numbers):
print(numbers[index])
index += 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_12524/68743835.py in <module>
8 numbers = {1, 2, 3, 4, 5}
9 while index < len(numbers):
---> 10 print(numbers[index])
11 index += 1
SYNTAX: iter(object,sentinel)
If the sentinel parameter is also provided, iter() returns an iterator until the sentinel
character isn't found.
In [18]:
# Program to demonstrate __iter__() methods
x=[1,2,3,4,5,6,7,8,]
value=x.__iter__()
print(value)
print(dir(x)) # to check whether __iter__ is present or not in iterable.
next() method:
1. Python next() function returns the next item of an iterator. It always return the current
value of the iterable and updates the state to the next value.
1. next() Parameters:
The next() function returns the next item from the iterator.
If the iterator is exhausted, it returns the default value passed as an argument.
If the default parameter is omitted and the iterator is exhausted, it raises the StopIteration
exception.
In [19]:
# Program to demonstrate __next__() method
x=[1,2,3,4,5,6,7,8,]
value=x.__iter__()
item1=value.__next__()
print(item1)
item2=value.__next__()
print(item2)
item3=value.__next__()
print(item3)
#print(dir(value)) # to check whether __super__ is present or not in iterable.
1
2
localhost:8888/nbconvert/html/UNIT-3 FUNCTIONAL PROGRAMMING.ipynb?download=false 13/27
6/8/22, 10:33 AM UNIT-3 FUNCTIONAL PROGRAMMING
3
In [27]:
#Note: Python has an elegant way to call __iter__() simply with the iter() function and
#function.
numbers = [1, 2, 3]
value = iter(numbers)
item1 = next(value)
print(item1)
item2 = next(value)
print(item2)
item3 = next(value)
print(item3)
item4 = next(value)
print(item4)
1
2
3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_12524/40794909.py in <module>
11 print(item3)
12
---> 13 item4 = next(value)
14 print(item4)
StopIteration:
In [20]:
st="python"
it2=iter(st)
print(next(it2))
print(it2.__next__())
print(next(it2))
print(next(it2))
print(next(it2))
print(next(it2))
print(next(it2))
p
y
t
h
o
n
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_9684/862733828.py in <module>
7 print(next(it2))
8 print(next(it2))
----> 9 print(next(it2))
StopIteration:
list1 = [1, 2, 3, 4, 5]
print(list1)
print(next(list1, -1))
print(next(list1, -1))
print(next(list1, -1))
print(next(list1, -1))
print(next(list1, -1))
print(next(list1, -1))
print(next(list1, -1))
In [30]:
def custom_for_loop(iterable, action_to_do):
iterator = iter(iterable)
done_looping = False
while not done_looping:
try:
item = next(iterator)
except StopIteration:
done_looping = True
else:
action_to_do(item)
numbers = [1, 2, 3, 4, 5]
custom_for_loop(numbers, print)
1
2
3
4
5
localhost:8888/nbconvert/html/UNIT-3 FUNCTIONAL PROGRAMMING.ipynb?download=false 15/27
6/8/22, 10:33 AM UNIT-3 FUNCTIONAL PROGRAMMING
NOTE: In fact, this is exactly how for loops work behind the scene. A for loop internally creates an
iterator object, and iterates over it calling the next method until a StopIteration exception is
encountered.
In [31]:
# The above code is equivalent to:
numbers = [1, 2, 3, 4, 5]
1
2
3
4
5
1. Create an iterator that returns numbers, starting with 1, and each sequence will increase by one (returning 1,2,3,4,5
etc.)
In [15]:
class Numbers:
def __iter__(self):
self.a=1
return self
def __next__(self):
x=self.a
self.a += 1
return x
obj=Numbers()
iter_obj=iter(obj)
print(next(iter_obj))
print(next(iter_obj))
print(next(iter_obj))
In [21]:
#iterator for squared numbers
class Sq:
def __init__(self):
self.a=0
def __iter__(self):
return self
def __next__(self):
if self.a<=9:
self.a=self.a+1
return self.a**2
else:
raise StopIteration
for i in Sq():
print(i)
1
4
9
16
25
36
49
64
81
100
3. CUSTOM ITERATORS FOR FIBONACCI SERIES
In [22]:
#Program to create iterator for fibbonacci series
class fib:
def __init__(self,stop):
self.a=-1
self.b=1
self.n=0
self.stop=stop
def __iter__(self):
return self
def __next__(self):
self.n+=1
if self.n<=self.stop:
c=self.a+self.b
self.a=self.b
self.b=c
return c
else:
raise StopIteration
#for i in fib(10):
# print(i,end=',')
f1=fib(5)
fit=iter(f1)
print(next(fit))
print(next(fit))
print(next(fit))
localhost:8888/nbconvert/html/UNIT-3 FUNCTIONAL PROGRAMMING.ipynb?download=false 17/27
6/8/22, 10:33 AM UNIT-3 FUNCTIONAL PROGRAMMING
print(next(fit))
print(next(fit))
print(next(fit))
0
1
1
2
3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_9684/3945298406.py in <module>
26 print(next(fit))
27 print(next(fit))
---> 28 print(next(fit))
~\AppData\Local\Temp/ipykernel_9684/3945298406.py in __next__(self)
16 return c
17 else:
---> 18 raise StopIteration
19 #for i in fib(10):
20 # print(i,end=',')
StopIteration:
In [23]:
#iterator to create our own range function
class myrange:
def __init__(self,sp,s=0,st=1):
self.s=s
self.sp=sp
self.st=st
def __iter__(self):
return self
def __next__(self):
if self.s<self.sp:
x=self.s
self.s=self.s+self.st
return x
else:
raise StopIteration
for i in myrange(10,2,2):
print(i)
2
4
6
8
4. Create a custom iterator class that generate numbers between min value and max value.
In [9]:
class generate_numbers:
def __init__(self, min_value, max_value):
self.current = min_value
self.high = max_value
def __iter__(self):
return self
def __next__(self):
if self.current > self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1
<class '__main__.generate_numbers'>
40
41
42
43
44
45
46
47
48
49
50
5. Create a custom iterator object to generate a sequence of even numbers such as 2, 4, 6, 8 and so on.
In [1]:
class Even:
def __init__(self, max):
self.n = 2
self.max = max
def __iter__(self):
return self
def __next__(self):
if self.n <= self.max:
result = self.n
self.n += 2
return result
else:
raise StopIteration
numbers = Even(10)
print(next(numbers))
print(next(numbers))
print(next(numbers))
2
localhost:8888/nbconvert/html/UNIT-3 FUNCTIONAL PROGRAMMING.ipynb?download=false 19/27
6/8/22, 10:33 AM UNIT-3 FUNCTIONAL PROGRAMMING
4
6
BENEFITS OF ITERATORS
1. Iterators are powerful tools when dealing with a large stream of data.
2. If we used regular lists to store these values, our computer would run out of memory.
3. With iterators, however, we can save resources as they return only one element at a time. So, in
theory, we can deal with infinite data in finite memory.
E. GENERATORS IN PYTHON:
WHY GENERATORS?
There is a lot of work in building an iterator in Python. We have to implement a class with
iter() and next()_ method, keep track of internal states, and raise StopIteration when
there are no values to be returned.
This is both lengthy and counterintuitive. Generator comes to the rescue in such
situations.
Python generators are a simple way of creating iterators. All the work we mentioned
above are automatically handled by generators in Python.
In [2]:
def fun():
print("function starts")
print("next statement of function")
return 0
fun()
function starts
next statement of function
0
Out[2]:
In [3]:
fun()
function starts
next statement of function
0
Out[3]:
In [30]:
def fun1():
i=0
print("function begins")
yield i
i+=1
print("function continue")
yield i
i+=1
print("function terminates")
yield i
obj=fun1()
print(type(fun1))
print(type(obj))
print(next(obj))
print(next(obj))
print(next(obj))
print(next(obj))
<class 'function'>
<class 'generator'>
function begins
0
function continue
1
function terminates
2
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_5404/1620009420.py in <module>
18 print(next(obj))
19 print(next(obj))
---> 20 print(next(obj))
StopIteration:
In [15]:
obj2=fun1()
for i in obj2:
print(i)
function begins
0
function continue
1
function terminates
2
In [1]:
# A Generator function with for loop
def gen_fun():
yield 10
yield 20
yield 30
10
20
30
NOTE: Generator function use ''yield'' keyword instead of ''return'' and it will return a value whenever it is called.
1. When called, it returns an object (iterator) but does not start execution immediately.
1. Methods like iter() and next() are implemented automatically. So we can iterate through the
items using next().
1. Once the function yields, the function is paused and the control is transferred to the caller.
1. Local variables and their states are remembered between successive calls.
1. Finally, when the function terminates, StopIteration is raised automatically on further calls.
1. It replace the return of a function to suspend its execution without destroying local
variables.
1. Yield statement function is executed from the last state from where the function get paused.
RETURN STATEMENT:
1. Return is generally used for the end of the execution and “returns” the result to the caller
statement.
In [31]:
# Programme to demonstrate yield keyword
# Use of yield
def printresult(String) :
for i in String:
if i == "a":
yield i
# initializing string
String = "Agapastala"
ans = 0
print ("The number of 'a' in word is : ", end = "" )
String = String.strip()
for j in printresult(String):
ans = ans + 1
print (ans)
In [21]:
# Program to show return statement
class Test:
def __init__(self):
self.str = "Agapastala"
self.x = "Ranbir Kapoor"
Agapastala
Ranbir Kapoor
2. According to its definition, they are similar to lambda function as lambda function is an
anonymous function, and generator functions are anonymous.
3. But when it comes to implementation, they are different, they are implemented similarly to list
comprehension does, and the only difference in implementation is, instead of using square
brackets('[]'), it uses round brackets('()').
4. The main and important difference between list comprehension and generator expression is
list comprehension returns a list of items, whereas generator expression returns an
iterable object.
In [25]:
# Creation of generator using generator expression.
x = 10
gen = (i for i in range(x) if i % 2 == 0) #(i for i in range(x) if i % 2 == 0) in this
list_ = [i for i in range(x) if i % 2 == 0] #and next is the for loop, followed by the
print(gen)
print(list_)
for j in gen:
print(j)
In [22]:
# Generator function
def even_generator():
n = 0
n += 2
yield n
n += 2
yield n
n += 2
yield n
numbers = even_generator()
print(next(numbers))
print(next(numbers))
print(next(numbers))
2
4
6
Then, we have called the next() method to retrieve elements from this iterator. The first yield
returns the value of n = 2.
The difference between return and yield is that the return statement terminates the function
completely while the yield statement pauses the function saving all its states for next successive
calls.
So, when we call yield for the second and third time, we get 4 and 6 respectively.
In [32]:
def even_generator(max):
n = 2
numbers = even_generator(4)
print(next(numbers))
print(next(numbers))
#print(next(numbers))
#Notice how we have never explicitly defined the __iter__() method, __next__() method,
#They are handled implicitly by generators making our program much simpler and easier t
2
4
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_5404/3768536747.py in <module>
10 print(next(numbers))
11 print(next(numbers))
---> 12 print(next(numbers))
13
14
StopIteration:
let's build a generator to produce an infinite stream of fibonacci numbers. The fibonacci
series is a series where the next element is the sum of the last two elements.
for i in infinite():
if i%4 == 0:
continue
elif i == 13:
break
else:
print(i)
1
2
3
5
6
7
9
10
11
In [ ]:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,.....
In [ ]:
def generate_fibonacci():
n1 = 0
yield n1
n2 = 1
yield n2
while True:
n1, n2 = n2, n1 + n2
yield n2
seq = generate_fibonacci()
print(next(seq))
print(next(seq))
print(next(seq))
print(next(seq))
print(next(seq))
NOTE: If we had used a for loop and a list to store this infinite series, we would have run out of
memory.However, with generators, we can keep accessing these items for as long as we want. It is
because we are just dealing with one item at a time.
1. MEMORY EFFICIENT: Generator Functions are memory efficient, as they save a lot of memory
while using generators. A normal function will return a sequence of items, but before giving the
result, it creates a sequence in memory and then gives us the result, whereas the generator
function produces one output at a time.
1. INFINITE SEQUENCE: We all are familiar that we can't store infinite sequences in a given
memory. This is where generators come into the picture. As generators can only produce one
item at a time, so they can present an infinite stream of data/sequence.
1. Create a generator function to generate an infinite stream of odd numbers and print the
first 10 elements.
In [ ]:
def generate_odd():
n = 1
while True:
yield n
n += 2
odd_generator = generate_odd()