0% found this document useful (0 votes)
71 views

UNIT - 3 Python

1. A class defines the attributes and behaviors of objects that will be created from it. Attributes are represented by variables and behaviors by methods. 2. When an object is created from a class, it is called an instance. Instance variables and methods allow access to the attributes and behaviors of that specific object. 3. The constructor method __init__() is used to initialize the attributes of an object when it is created. It takes self as the first parameter which refers to the instance.

Uploaded by

Nidisha
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
71 views

UNIT - 3 Python

1. A class defines the attributes and behaviors of objects that will be created from it. Attributes are represented by variables and behaviors by methods. 2. When an object is created from a class, it is called an instance. Instance variables and methods allow access to the attributes and behaviors of that specific object. 3. The constructor method __init__() is used to initialize the attributes of an object when it is created. It takes self as the first parameter which refers to the instance.

Uploaded by

Nidisha
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 53

Classes and Objects

Introduction
A class is a model or plan to create objects. This means, we write a class
with the attributes and actions of objects. Attributes are represented
by variables and actions are performed by methods. So, a class contains variable
and methods. The same variables and methods are also available in the objects
because they are created from the class. These variables are also called
'instance variables' because they are created inside the instance (i.e. object).
Please remember the difference between a function and a method. A
function written inside a class is called a method.Generally, a method is called
using one of the following two ways:
classname.methodname()
instancename.methodname()
The general format of a class is given as follows:
class Classname(object):
attributes
def __init__(self):
def method1():
def method2():
Creating a Class
A class is created with the keyword class and then writing the
Classname. After the Classname, ‘object’ is written inside the Classname. This
object represents the base class name from where all classes in Python are
derived. Even our own classes are also derived from object class. Hence we
should mention ‘object’ in the parenthesis. Writing ‘object’ is not compulsory
The docstring is a string which is written using triple double quotes or
triple single quotes that gives the complete description about the class and its
usage. The docstring is used to create documentation file and it is optional.
‘attributes’ are nothing but variables that contains data. __init__(self) is a
special method to initialize the variables.method1(), method2() etc are methods
that are intended to process variables.
If we take Student class, we can write code in the class that specifies the
attributes and actions performed by any student. For example, a student has
attributes like name, age, marks, etc. these attributes should be written inside the
Student class as variables. Similarly, a student can perform actions like talking,
writing, reading etc. these actions should be represented by methods in the
Student class. So, the class Student contains these attributes and actions, as
shown here:
class Student: #another way is class Student(object):
#the below block defines attributes
def __init__(self):
self.name='Vishnu'
self.age=20
self.marks=900

#the below block defines a method


def talk(self):
print("Hi I am ", self.name)
print("My age is ", self.age)
print("My marks are ", self.marks)
The keyword class is used to declare a class. After this we should write the class
name. So Student is our class name. Generally, a class name should start with a
capital letter, hence ‘S’ is capital in Student. In the class, we write attributes and
methods. Since in Python, we cannot declare variables, we have written the
variables inside a special method i.e. __init__(). This method is useful to
initialize the variables. Hence the name ‘init’. The method name has 2
underscores before and after. This indicates that this method is internally
defined and we cannot call this method explicitly. The parameter ‘self’ is
written after the method name in the parenthesis. ‘self’ is a variable that refers
to current class instance. When we create an instance for the Student class, a
separate memory block is allocated on the heap and that memory location is by
default stored in ‘self’. The instance contains the variables name, age, marks
which are called instance variables. To refer to instance variables, we can use
the dot operator notation along with self as ‘self.name’, ‘self.age’, ‘self.marks’.
The method ‘talk()’ takes the ‘self’ variable as parameter. This method
displays the values of the variables by referring them using ‘self’. The methods
that act on instances (Objects) of a class are called instance methods. Instance
methods use ‘self’ as the first parameter that refers to the location of the
instance in the memory. Since instance methods know the location of instance,
they can act on the instance variables. In the previous code, the two methods
__init__(self) and talk(self) are called instance methods.
In the Student class, a student is talking to us through talk() method. He is
introducing himself to us as shown here:
Hi, I am Vishnu
My age is 20
My marks are 900
This is what the talk() method displays. Writing a class like this is not
sufficient. It should be used. To use a class, we should create an instance (or
object) to the class. Instance creation represents allotting memory necessary to
store the actual data of the variables i.e., Vishnu, 20 and 900. To create an
instance, the syntax is: instancename=classname()
To create an instance or object to the Student class, we can write as:
s1=Student(). Here ‘s1’ is the instance name. When we create an instance like
this, the following steps will take place internally:
1. A block of memory is allocated on heap. How much memory is to be
allocated is decided from the attributes and methods available in the Student
class
2. After allocating the memory block, the special method by the name
‘__init__(self)’ is called internally. This method stores the initial data into
the variables. Since this method is useful to construct the instance, it is called
‘constructor’
3. Finally, the allocated memory location address of the instance is returned
into ‘s1’ variable. To see this memory location in decimal number format,
we can use id() function as id(s1)
Now ‘s1’ refers to the instance of the Student class. Hence any variables or
methods in the instance can be referenced by ;s1’ using dot operator as:
s1.name#this refers to data in name variable “Vishnu”
s1.age#this refers to data in age variable “20”
s1.marks# this refers to data in marks variable “900”
s1.talk()this calls the talk() method
The dot operator takes the instance name at its left and the member of the
instance at the right hand side
class Student:
#the below block defines attributes
def __init__(self): OUTPUT
self.name='Vishnu' Hi, I am Vishnu
self.age=20 My age is 20
self.marks=900 My marks are 900
#the below block defines a method
def talk(self):
print("Hi I am ", self.name)
print("My age is ", self.age)
print("My marks are ", self.marks)
s1=Student()
s1.talk()
The Self Variable
‘self’ is a default variable that contains the memory address of the
instance of the current class. So, we can use ‘self’ to refer to all the instance
variables and instance methods.
When an instance to the class is created, the instance name contains the
meory location of the instance. This memory location is internally passed to
‘self’. For example, we create an instance to Student class as: s1.Student()
Here ‘s1’ contains the memory address of the instance. This memory address is
internally and by default passed to ‘self’ variable. Since ‘self’ knows the
memory address of the instance, it can refer to all the members of the instance.
We use ‘self’ in 2 ways:
1. The ‘self’ variable is used as first parameter in the constructor as:
def __init__(self):
2. ‘self’ can be used as first parameter in the instance method as:
def talk(self)
Here, talk() is instance method as it acts on the instance variable. If this
method wants to act on the instance variable, it should know the memory
location of the instance variable. That memory location is by default
available to the talk() method through ‘self’.
Constructor
A constructor is a special method that is used to initialize the instance variables
of a class. In the constructor, we create the instance variables and initialize them
with some starting values. The first parameter of the constructor will be ‘self’
variable that contains the memory address of the instance. For example
def __init__(self):
self.name='Vishnu'
self.age=20
self.marks=900
Here, the constructor has only one parameter i.e., ‘self’. Using ‘self.name’ and
‘self.marks’, we can access the instance variables of the class. A constructor is
called at the time of creating an instance. So, the above constructor will be
called when we create an instance as: s1=Student(). Here ‘s1’ is the name of
the instance. Observe the empty parentheses after the class name ‘Student’.
These empty parentheses represent that we are not passing any values to the
constructor. Suppose, we want to pass some values to the constructor, then we
have to pass them in the parentheses after the class name. We can write a
constructor with some parameters in addition to ‘self’ as:
def __init__(self, n ='', m=0):
self.name = n
self.marks = m
Here, the formal arguments are ‘n’ and ‘m’ whose default values are given as ‘’
(None) and 0 (zero). Hence, if we do not pass any values to constructor at the
time of creating an instance, the default values of these formal arguments are
stored into name and marks variables. For example, s1=Student()
Since we are not passing any values to the instance, None and zero are stored
into name and marks. Suppose we create an instance as:
s1=Student(‘Lakshmi’,880). In this case we are passing 2 actual
arguments “Lakshmi’ and 880 to the Student instance. Hence these values are
sent to the arguments ‘n’ and ‘m’ and from there stored into name and marks
variables.
class Student:
def __init__(self, n ='', m=0):
self.name = n OUTPUT
self.marks = m Hi
def display(self): Your marks 0
print('Hi', self.name) Hi Lakshmi
print('Your marks', self.marks) Your marks 880
s = Student()
s.display()
s1 = Student('Lakshmi', 880)
s1.display()
Types of Variables
The variables which are written inside a class are of 2 types:
1. Instance variables
2. Class variables or Static variables
Instance variables are the variables whose separate copy is created in every
instance (or object). For example, if 'x' is an instance variable and if we create 3
instances, there will be 3 copies of 'x' in these 3 instances. When we modify the
copy of 'x' in any instance, it will not modify the other two copies.
class Sample:
def __init__(self):
self.x = 10
def modify(self): OUTPUT
self.x+=1 x in s1= 10
s1 = Sample() x in s2= 10
s2 = Sample() x in s1= 11
x in s2= 10
print('x in s1= ', s1.x)
print('x in s2= ', s2.x)
s1.modify()
print('x in s1= ', s1.x)
print('x in s2= ', s2.x)
Instance variables are defined and initialized using a constructor with ‘self’
parameter. Also, to access instance variables, we need instance methods with
‘self’ as first parameter. It is possible that the instance methods may have other
parameters in addition to the ‘self’ parameter. To access the instance variables,
we can use self.variable. it is also possible to access the instance variables from
outside the class as: instancename.variable, e.g.s1.x
Unlike instance variables, class variables are the variables whose single copy is
available to all the instances of the class. If we modify the copy of class variable
in an instance, it will modify all the copies in the other instances. For example,
if ‘x’ is a class variable and if we create 3 instances, the same copy of ‘x’ is
passed to these 3 instances. When we modify the copy of ‘x’ in any instance
using a class method, the modified copy is sent to the other 2 instances. Class
variables are also called static variables.
class Sample:
x = 10
@classmethod
def modify(cls): OUTPUT
cls.x+=1 x in s1= 10
s1 = Sample() x in s2= 10
s2 = Sample() x in s1= 11
print('x in s1= ', s1.x) x in s2= 11
print('x in s2= ', s2.x)
s1.modify()
print('x in s1= ', s1.x)
print('x in s2= ', s2.x)
In the above program the class variable ‘x’ is defined in the class and initialized
with value 10. A method by the name ‘modify’ is used to modify the value of
‘x’. This method is called ‘class method’ since it is acting on the class variable.
To mark this method as class method, we should use built-in decorator
statement @classmethod.
A class method contains first parameter by default as ‘cls’ with which we can
access the class variables. For example, to refer to the class variable ‘x’, we can
use ‘cls.x’. we can also write other parameter in the class method in addition to
the ‘cls’ parameter. the class variables are defined directly in the class. To
access class variables, we need class methods with ‘cls’ as first parameter. We
can access the class variables using the class methods as: cls.variable. If we
want to access the class variables from outside the class, we can use:
classname.variable, eg.Sample.x
Namespaces
A namespace represents a memory block where names are mapped (or linked)
to objects. Suppose we write: n = 10
Here, 'n' is the name given to the integer object 10. Please recollect that
numbers, strings, lists etc. are all considered as objects in Python. The name 'n'
is linked to 10 in the namespace. A class maintains its own namespace, called
‘class namespace’. In the class namespace, the names are mapped to class
variables. Similarly, every instance will have its own name space, called
‘instance namespace’. In the instance namespace, the names are mapped to
instance variables.

class Student:
n=10
print(Student.n) OUTPUT
Student.n+=1 10
print(Student.n) 11
s1=Student() 11
print(s1.n) 11
s2=Student()
print(s2.n)
We know that a single copy of variable is shared by all the instances. So, if the
class variable is modified in the class namespace, since same copy of the
variable is modified, the modified copy is available to all the instances.
What happens when the class variable is modified in the instance namespace?
Since every instance will have its own namespace, if the class variable is
modified in one instance namespace, It will not affect the variable in the other
instance namespaces. This is shown in below figure. To access the class
variable at the instance level, we have to create instance first and then refer to
the variable as instancename.variable.

class Student:
n=10
s1 = Student() OUTPUT
print(s1.n) 10
s1.n+=1 11
print(s1.n) 10
s2 = Student()
print(s2.n)
Types of Methods
The purpose of a method is to process the variables provided in the class or in
the method. The variables declared in the class are called class variables (or
static variables) and the variables declared in the constructor are called instance
variables. We can classify the methods in the following 3 types:
1. Instance Methods
(a)Accessor methods
(b)Mutator methods
2. Class Methods
3. Static Methods
Instance Methods
Instance methods are the methods which act upon the instance variables of the
class. Instance methods are bound to instances (or objects) and hence called as:
instancename.method(). Since instance variables are available in the instance,
instance methods need to know the memory address of the instance. This is
provided through 'self' variable by default as first parameter for the instance
method. While calling the instance methods, we need not pass any value to
the 'self' variable.
class Student:
#constructor OUTPUT
def __init__(self,n='',m=0): Number of students2
Enter name: Lakshmi
self.name=n
Enter marks: 100
self.marks=m Hi, I am Lakshmi
#instance method My marks are 100
def display(self): ___________________
print("Hi, I am ",self.name) Enter name: Rahul
Enter marks: 150
print("My marks are ",self.marks)
Hi, I am Rahul
My marks are 150
n=int(input("Number of students")) ___________________
i=0
while(i<n):
name=input("Enter name: ")
marks=input("Enter marks: ")

s1=Student(name,marks)
s1.display()
i+=1
print("___________________")

Instance methods are of 2 types:


(a)accessor methods
(b)mutator methods.
Accessor methods simply access or read data of the variables. They do not
modify the data in the variables. Accessor methods are generally written in the
form of getXXX() and hence they are also called getter methods.
For example,
def getName(self):
return self.name
Here, getName() is an accessor method since it is reading and returning the
value of 'name' instance variable. It is not modifying the value of the name
variable. On the other hand Mutator methods are the methods which not only
read the data but also modify them. They are written in the form of setXXX()
and hence they are also called setter methods. For example,
def setName(self, name):
self.name = name
Here, setName() is a mutator method since it is modifying the value of 'name'
variable by storing new name. In the method body, 'self.name' represents the
instance variable 'name' and the right hand side 'name' indicates the parameter
that receives the new value from outside. Since mutator methods define the
instance variables and store data, we need not write the constructor in the class
to initialize the instance variables.
class Student:
#mutator method
def setName(self,name):
self.name=name
#accessor method
def getName(self):
return self.name
#mutator method
def setMarks(self,marks):
self.marks=marks
#accessor method
def getMarks(self):
return self.marks

n=int(input("Number of students"))
i=0
while(i<n):
s=Student()
name=input("Enter name") OUTPUT
Number of students1
s.setName(name)
Enter nameLakshmi
marks=input("Enter marks") Enter marks100
s.setMarks(marks) Hi, I am Lakshmi
Your marks are 100
print("Hi, I am ",s.getName())
print("Your marks are ",s.getMarks())
i+=1
Class Methods
These methods act on class level. Class methods are the methods which act on
the class variables or static variables. These methods are written using
@classmethod decorator above them. By default, the first parameter for class
methods is 'cls' which refers to the class itself. For example, 'cls.var' is the
format to refer to the class variable. These methods are generally called using
the classname.method().

class Bird: OUTPUT


wings = 2 Sparrow flies with 2 wings
Pigeon flies with 2 wings
@classmethod
def fly(cls, name):
print('{} flies with {} wings'.format(name,
cls.wings))
Bird.fly('Sparrow')
Bird.fly('Pigeon')

Static Methods
We need static methods when the processing is at the class level but we need
not involve the class or instances. Static methods are used when some
processing is related to the class but does not need the class or its instances to
perform any work.Setting environmental variables, counting the number of
instances of the class or changing an attribute in another class, etc. are the tasks
related to a class. Such tasks are handled by static methods. Also, static methods
can be used to accept some values, process them and return the result. In this
case the involvement of neither the class nor the objects is needed. Static
methods are written with a decorator @staticmethod above them. Static
methods are called in the form of classname.method().
class Myclass:
n=0
def __init__(self):
Myclass.n = Myclass.n+1
@staticmethod
def noObjects():
print('No. of instances created: ', Myclass.n)
obj1 = Myclass()
obj2 = Myclass() OUTPUT
obj3 = Myclass() No. of instances created: 3
Myclass.noObjects()
In the next program, we accept a number from the keyboard and return the
result of its square root value. Here, there is no need of class or object and hence
we can write a static method to perform this task.
#a static method to find square root value
import math
class Sample: OUTPUT
@staticmethod Enter a number:4
def calculate(x): The square root of 4.0 is 2.00
result = math.sqrt(x)
return result
num = float(input('Enter a number:'))
res = Sample.calculate(num)
print('The square root of {} is {:.2f}'.format(num, res))
Passing Members of One Class to Another Class
It is possible to pass the members (i.e. attributes and methods) of a class to
another class. Let's create an instance of Emp class as:e = Emp()
Then pass this instance 'e' to a method of other class, as:
Myclass.mymethod(e)
Here, Myclass is the other class and mymethod() is a static method that belongs
to Myclass. In Myclass, the method mymethod() will be declared as a static
method as it acts neither on the class variables nor instance variables of
Myclass. The purpose of mymethod() is to change the attribute of Emp class
#class contains employee details
class Emp:
#constructor
def __init__(self,id,name,salary):
self.id=id
self.name=name
self.salary=salary

#instance method
def display(self):
print("ID= ",self.id)
print("Name= ",self.name)
print("Salary= ",self.salary)

#new class displays employee details


class MyClass:
#method to receive Emp class instance and display
employee details
@staticmethod
def mymethod(e): OUTPUT
#increment salary by 1000 ID= 10
e.salary+=1000 Name= Rakesh
e.display() Salary= 16000

#create Emp class instance e


e=Emp(10,"Rakesh",15000)
#call static method of MyClass and pass e
MyClass.mymethod(e)

Another example for static method


#another example for static method
class Myclass:
@staticmethod
def mymethod(x, n):
result = x**n
print('{} to the power of {} is {}'.format(x, n,
result))
#call the static method OUTPUT
Myclass.mymethod(5, 3) 5 to the power of 3 is 125
Myclass.mymethod(5, 4) 5 to the power of 4 is 625
Myclass.mymethod(6,2) 6 to the power of 2 is 36
Myclass.mymethod(6,3) 6 to the power of 3 is 216

Inner Classes
Writing a class within another class is called creating an inner class or nested
class. For example, if we write class B inside class A, then B is called inner
class or nested class. Inner classes are useful when we want to sub group the
data of a class. For example, let's take a person's data like name, age, date of
birth etc. Here, name contains a single value like ‘Abcd', age contains a single
value like ‘21' but the date of birth does not contain a single value. DoB
contains three values like date, month and year. So, we need to take these three
values as a sub group. Hence it is better to write date of birth as a separate class
Dob inside the Person class. This Dob will contain instance variables dd, mm
and yy which represent the date of birth details of the person.
#A python program to create Dob class within Person class
class Person:
def __init__(self):
self.name="Charles"
self.dob=self.Dob()
def display(self):
print("Name= ",self.name)

#inner class
OUTPUT
class Dob: Name= Charles
def __init__(self): DOB= 10/5/1988
self.dd=10
self.mm=5
self.yy=1988
def display(self):
print("DOB=
{}/{}/{}".format(self.dd,self.mm,self.yy))

#creating an instance of Person


p=Person()
p.display()

#create inner class object


x=p.dob
x.display()

One more example

#A python program to create another version of Dob class


within
#Person class
class Person:
def __init__(self):
self.name="Charles"
def display(self):
print("Name= ",self.name)
#inner class
class Dob: OUTPUT
def __init__(self): Name= Charles
DOB= 10/5/1988
self.dd=10
1988
self.mm=5
self.yy=1988
def display(self):
print("DOB=
{}/{}/{}".format(self.dd,self.mm,self.yy))

#creating an instance of Person


p=Person()
p.display()

#create Dob class object as sub object to Person class


object
x=Person().Dob()
x.display()
print(x.yy)
CHAPER 2
Inheritance and Polymorphism
Software development is a team effort. Several programmers will work as a team to
develop software. When a programmer develops a class, he will use its features by creating
an instance to it. When another programmer wants to create another class which is similar to
the class already created, then he need not create the class from the scratch. He can simply
use the features of the existing class in creating his own class.
A programmer in the software development is creating Teacher class with setter ()
and getter () methods as shown below. Save this code in a file called ‘teacher.py’
#this is Teacher class.
class Teacher:
def setid(self,id):
self.id=id
def getid(self):
return self.id
def setname(self,name):
self.name=name
def getname(self):
return self.name
def setaddress(self,address):
self.address=address
def getaddress(self):
return self.address
When the programmer wants to use this Teacher class that is available in teacher.py file, he
can simply import this class into his program and use it as shown here:

#save this code as inh.py file


#using Teacher class
from teacher import Teacher
#create instance OUTPUT
t=Teacher() id= 10
name= Prakash
#store data into the instance address= Delhi
t.setid(10)
t.setname("Prakash")
t.setaddress("Delhi")
#retrieve data
print("id= ",t.getid())
print("name= ",t.getname())
print("address= ",t.getaddress())
Once the Teacher class is completed, the programmer stored teacher.py program in a
central database that is available to all the members of the team. So, Teacher class is made
available through the module teacher.py
Programmer 1
teacher.py

Programmer 2

central database

Now another programmer in the same team wants to create Student class. He is planning the
Student class without considering the Teacher class as shown below:
#Student class
class Student:
def setid(self,id):
self.id=id
def getid(self):
return self.id
def setname(self,name):
self.name=name
def getname(self):
return self.name
def setaddress(self,address):
self.address=address
def getaddress(self):
returnself.address
def setmarks(self,marks):
self.marks=marks
def getmarks(self):
return self.marks
Now, the second programmer who created this Student class and saved it as student.py can
use it whenever he needs
#using Student class. Save this as inh.py
from student import Student
s=Student()
s.setid(100) OUTPUT
s.setname("Rakesh") ID= 100
s.setaddress("Bangalore")
Name= Rakesh
s.setmarks(970)
print("ID= ",s.getid()) Address= Bangalore
print("Name= ",s.getname()) Marks= 970
print("Address= ",s.getaddress())
print("Marks= ",s.getmarks())
If we compare the Teacher class and Student classes, we can understand that 75% of the code
is same in both the classes. Most of the code being planned by the second programmer in his
Student class is already available in the Teacher class. Then why doesn’t he use it for his
advantage? Our idea is this: instead of creating a new class altogether, he can reuse the code
which is already available. This is shown in below program:
#Python program to create Student class by deriving it from the
#Teacher class
from teacher import Teacher
class Student(Teacher):
def setmarks(self,marks):
self.marks=marks
def getmarks(self):
return self.marks
The preceding code will be same as the first version of the Student class. In the first statement
we are importing Teacher class from teacher module so that the Teacher class is now
available to this program. Then we are creating Student class as:
class Student(Teacher):
This means the Student class is derived from Teacher class. All the members of Teacher class
are available to the Student class. Hence we can use them without rewriting them in the
Student class. In addition, the following two methods are needed by the Student class but not
available in the Teacher class:
def setmarks(self,marks):
def getmarks(self):
In other words, we created Student class from the Teacher class. This is called Inheritance.
The original class i.e., Teacher class is called base class or super class and the newly created
class i.e., the Student class is called the Sub class or derived class. Deriving new classes
from the existing classes such that the new classes inherit all the members of the existing
classes is called Inheritance. The syntax for inheritance is:
class Subclass(Baseclass)
The advantages of inheritance are:
 By inheritance, a programmer can develop the classes very easily. Hence
programmer’s productivity is increased. Productivity is a term refers to the code
developed by the programmer in a given span of time.
 If the programmer uses inheritance, he will be able to develop more code in less time
 More profits for the organization and better growth for the programmer.
In inheritance, we always create only the sub class object. Generally, we do not create super
class object. The reason is clear. Since all the members of the super class are available to sub
class, when we create an object, we can access the members of both the super and sub
classes. But if we create an object to super class, we can access only the super class members
and not the sub class members
Constructors in Inheritance
In previous programs, we have inherited the Student class from Teacher class. All the
methods and the variables in those methods of the Teacher class (base class) are accessible to
the Student class (sub class). Are the constructors of the base class accessible to the sub class
or not-is the next question we will answer. In the below program, we are taking a super class
by the name ‘Father’ and derived a sub class ‘Son’ from it. The Father class has a constructor
where a variable ‘property’ is declared and initialized with 800000.00. When Son is created
from Father, this constructor is by default available to Son class. When we call the method of
the super class using sub class object, it will display the value of ‘property’ variable.
#program to access the base class constructor from sub #class
class Father:
def __init__(self):
OUTPUT
self.property=800000.00
Father's property= 800000.0

def display_property(self):
print("Father's property= ",self.property)

class Son(Father):
pass #we dont want to write anything in the subclass
#create instance of sub class
s=Son()
s.display_property()
Overriding Super Class Constructors and Methods
When the programmer writes a constructor in the sub class, the super class constructor
is not available to the sub class. In this case, only the sub class constructor is accessible from
the sub class object. That means the sub class constructor is replacing the super class
constructor. This is called constructor overriding. Similarly in the sub class, if we write a
method with exactly same name as that of super class method, it will override the super class
method. This is called method overriding.
#constructor and method overriding
class Father:
def __init__(self):
self.property=800000

def display_property(self):
print("Father's property= ",self.property)

class Son(Father):
OUTPUT
def __init__(self):
Son's property= 200000.0
self.property=200000
def display_property(self):
print("Child’s property= ",self.property)

#create instance of sub class


s=Son()
s.display_property()
In the sub class we created a constructor and a method with exactly same names as those of
super class. When we refer to them, only the sub class constructor and method are executed.
The base class constructor and method are not available to the sub class object. That means
they are overridden. In this case, how to call the super class constructor so that we can access
the father’s property from the son’s class? For this purpose, we should call the constructor of
the super class from the constructor of the sub class using the super() method.
The super () Method
super () is a built in method which is useful to call the super class constructor or methods
from the sub class. Any constructor written in the super class is not available to the sub class
if the sub class has a constructor. This is done by calling the super class constructor using the
super () method from inside the sub class constructor. super () is a built-in method in Python
that contains the history of super class methods. Hence we can use super () to refer to super
class constructor and methods from a sub class.
super().__init__() #call super class constructor
super().__init__(arguments) #call super class constructor and pass
#arguments
super().method() #call super class method
When there is a constructor with parameters in the super class, we have to create another
constructor with parameters in the sub class and call the super class constructor using super()
from the sub class constructor.
class Father:
def __init__(self,property=0):
self.property=property

def display_property(self):
print("Father's property= ",self.property)

class Son(Father):
def __init__(self,property1=0,property=0):
super().__init__(property)
self.property1=property1 OUTPUT
Total property= 1000000.0

def display_property(self):
print("Total property= ", self.property1+ self.property)

#create instance of sub class


s=Son(200000.00,800000.00)
s.display_property()
To understand the use of super() in a better way, let us consider another program
class Square:
def __init__(self,x):
self.x=x
def area(self):
print("area of square is ",self.x*self.x)

class Rectangle(Square): OUTPUT


Enter the first measurement10
Enter the second measurement5.5
area of square is 100.0
Area of rectangle 55.0
def __init__(self,x,y):
super().__init__(x)
self.y=y
def area(self):
super().area()
print("Area of rectangle ",self.x*self.y)

a=float(input("Enter the first measurement"))


b=float(input("Enter the second measurement"))
r=Rectangle(a,b)
r.area()
Types of Inheritance
The main advantage of inheritance is code reusability. The members of the super class
are reusable in the sub classes. All classes in Python are built from a single super class called
‘object’. If a programmer creates his own classes, by default object class will become super
class for them internally. This is the reason, sometimes while creating any new class, we
mention the object class name in parenthesis as: class myclass(object):
Here, we are indicating that object is the super class for Myclass. Writing object class name is
not mandatory
class myclass:
There are two types of inheritance. They are:
1. Single Inheritance
2. Multiple Inheritance
Single Inheritance
Deriving one or more sub classes from a single base class is called ‘single
inheritance’. In single inheritance, we always have only one base class, but there can be ‘n’
number of sub classes derived from it.

Bank

AndhraBank StateBank

#single inheritance
class Bank(object):
cash=1000
@classmethod
def available_cash(cls):
print(cls.cash)
OUTPUT
class AndhraBank(Bank):
pass 1000
class StateBank(Bank): 3000
cash=2000
@classmethod
def available_cash(cls):
print(cls.cash+Bank.cash)
a=AndhraBank()
a.available_cash()
s=StateBank()
s.available_cash()
In the above program, we are deriving 2 sub classes AndhraBank and StateBank from the
single base class Bank. All the memebrs of Bank class will be vailable to the sub classes. In
the Bank class, we have a variable ‘cash’ and a method to display that is available_cash().
Here the variable ‘cash’ is declared in the class and initialized to 1000. The available_cash()
is a class method that is accessing the variable as ‘cls.cash’. When we derive AndhraBank
class from Bank class, the ‘cash’ variable and available_cash() method is accessible to
AndhraBank class. Similarly we can derive another subclass by the name StateBank from
Bank class as:
class StateBank(Bank):
cash=2000
@classmethod
def available_cash(cls):
print(cls.cash+Bank.cash)
Here StateBank has its own class variable ‘cash’ that contains 2000. So the total cash
available to StateBank is 1000+2000=3000. Observe the last line in the code:
print(cls.cash+Bank.cash)
Here, ‘cls.cash’ represents the current class’s cash variable and ‘Bank.cash’ represents the
Bank base class cash variable.
Multiple Inheritance
Deriving sub classes from multiple base classes is called ‘multiple inheritance’. In this type
of inheritance, there will be more than one super class and there may be one or more sub
classes. All the members of the super classes are by default available to sub classes and the
sub classes in turn can have their own members. The syntax is:
class Subclass(Baseclass1,Baseclass2,…):

Father Mother

Child

#multiple inheritance
class Father:
OUTPUT
def height(self):
Childs inherited qualities are:
print("Height is 6.0 foot")
Height is 6.0 foot
Color is brown
class Mother:
def color(self):
print("Color is brown")

class Child(Father, Mother):


pass
c=Child()
print("Childs inherited qualities are: ")
c.height()
c.color()
Polymorphism
Polymorphism is a word that came from two Greek words, poly means many and morphos
means forms. If something exhibits various forms, it is called polymorphism. Assume that we
have wheat flour. Using this wheat flour, we can make burgers, rotis, and bread. It means
same wheat flour is taking different edible forms and hence we can say wheat flour is
exhibiting polymorphism.
In programming, if a variable, object or method exhibits different behavior in different
contexts, it is called polymorphism
Method Overloading
If a method is written such that it can perform more than one task, it is called method
overloading. We see method overloading in the languages like Java. For example we can call
the method as:
sum(10,15)
sum(10,15,20)
In the first call, we are passing two arguments and in the second call, we are passing three
arguments. It means the sum() method is performing two distinct operations. This is called
method overloading. In Java, to achieve this, we write two sum () methods with different
number of parameters as:
sum(int a, int b){}
sum(int a, int b, int c){}
Method overloading is not available in Python. Writing more than one method with the same
name is not possible in Python. So, we can achieve method overloading by writing same
method with several parameters. The method performs the operation depending on the
number of arguments passed in the method call. For example, we can write a sum () method
with default values ‘None’ for the arguments as:
def sum(self,a=None,b=None,c=None):
if a!=None and b!=None and c!=None:
print("Sum of three= ",a+b+c)
elif a!=None and b!=None:
print("Sum of two= ",a+b)
Here, sum () has 3 arguments a, b, c whose values by default ‘None’. This ‘None’ indicates
nothing or no value and similar to ‘num’ in languages like Java. While calling this method, if
the user enters 3 values, then the arguments a, b, and c will not be ‘None’. They get the
values entered by the user. If the user enters only 2 values, then the first two arguments a and
b will take those values and the third argument c will become ‘None’.
#method overloading
class Myclass:
def sum(self,a=None,b=None,c=None):
if a!=None and b!=None and c!=None:
print("Sum of three= ",a+b+c)
elif a!=None and b!=None:
print("Sum of two= ",a+b)
else:
print("Please enter two or three arguments")

#call sum() using object OUTPUT


m=Myclass() Sum of three= 15
m.sum(10,2,3) Sum of two= 26.0
m.sum(10.5,15.5) Please enter two or three arguments
m.sum(100)
Method Overriding
When there is a method in the super class, writing the same method in the sub class so that it
replaces the super class method is called ‘method overriding’. In inheritance, if we create
super class object, we can access all the members of the super class but not the members of
the sub class. But if we create sub class object, then both the super class and sub class
members are available since the sub class object contains a copy of the super class. Hence, in
inheritance we always create sub class object
#method overriding
import math
class Square:
def area(self,x):
print("Square area= ",x*x) OUTPUT
class Circle(Square): Circle area= 706.8583470577034
def area(self,x):
print("Circle area= ",(math.pi*x*x))

c=Circle()
c.area(15)
Chapter 3
Abstract Classes and Interfaces
Abstract Method and Abstract Class
An abstract method is a method whose action is redefined in the sub classes as per the
requirement of the objects. Generally abstract methods are written without body since their
body will be defined in the sub classes. But it is possible to write an abstract method with
body also. To mark a method as abstract, we should use the decorator @abstractmethod. On
the other hand, a concrete method is a method with body.
An abstract class is a class that generally contains some abstract methods. Since,
abstract class contains abstract methods whose implementation or body is later defined in the
sub classes. It is not possible to estimate the total memory required to create the object for the
abstract class. So PVM can’t create objects to an abstract class.
Once an abstract class is written, we should create sub classes and all the abstract
methods should be implemented in the sub classes. Then it is possible to create objects to the
sub classes.
Let us create a class Myclass as an abstract super class with an abstract method
calculate (). This method does not have any body within it. The way to create an abstract
class is to derive it from a meta class ABC that belongs to abc (abstract base class) module
as:
class Abstractclass(ABC):
Since all abstract classes should be derived from the meta class ABC which belongs to abc
module, we should import this module into our program. A meta class is a class that defines
the behavior of other classes. The meta class ABC defines that the class which is derived
from it becomes an abstract class. To import abc module’s ABC class and abstract method
decorator we can write as follows:
from abc import ABC,abstractmethod
or
from abc import *
Now, our abstract class Myclass should be derived from ABC class as:
class Myclass(ABC):
@abstractmethod
def calculate(self,x):
pass
In the above code, Myclass is an abstract class since it is derived from ABC meta class. This
class has an abstract method calculate() that does not contain any code. We used
@abstractmethod decorator to specify that this is an abstract method. We have to write sub
classes where this abstract method is written with its body.
#abstarct class example
from abc import ABC,abstractmethod
class Myclass(ABC):
@abstractmethod
def calculate(self,x):
pass OUTPUT
#this is the sub class of Myclass Square value: 4
class Sub1(Myclass): Cube: 8
def calculate(self,x):
print("Square value: ",x*x)
#second sub class
class Sub2(Myclass):
def calculate(self,x):
print("Cube: ",x**3)
#Sub1 class object
obj1=Sub1()
obj1.calculate(2)
#Sub2 class object
obj2=Sub2()
obj2.calculate(2)
Interfaces in Python
An abstract class is a class which contains some abstract methods as well as concrete
methods also. Imagine a class that contains only abstract methods and there are no concrete
methods. It becomes an interface. Interface is an abstract class but it contains only abstract
methods. None of the methods in the interface will have body. Only method headers will be
written in the interface. So an interface can be defined as a specification of method headers.
Since, we write only abstract methods in the interface, there is possibility for providing
different implementations for those abstract methods depending on the requirements of
objects. In the languages like Java, an interface is created using the key ‘interface’ but in
Python an interface is created as an abstract class only. The interface concept is not explicitly
available in Python. We have to use abstract classes as interfaces in Python.
Since an interface contains methods without body, it is not possible to create objects
to an interface. In this case, we can create sub classes where we can implement all the
methods of the interface. Since the sub classes will have all the methods with body, it is
possible to create objects to the sub classes.
Consider the below class:
#this class works with Oracle database only
class Oracle:
def connect(self):
print("Connecting to Oracle database")
def disconnect(self):
print("Disconnected from Oracle")
This class has a limitation. It can connect only to Oracle database. If a client (user) using any
other database uses this code to connect to his database, this code will not work. So, the
programmer is asked to design his code in such a way that it is used to connect to any
database in the world.
One way is to write several classes, each to connect to a particular database. Thus,
considering all the databases available in the world, the programmer has to write a lot of
classes. This takes a lot of time and effort. Interface helps to solve this problem. The
programmer writes an interface ‘Myclass’ with abstract methods as shown here:
class Myclass(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def disconnect(self):
pass
We can’t create objects to the interface. So, we need classes where all these methods of the
interface are implemented to connect to various databases. Consider the sub class:
#subclass
class Oracle(Myclass):
def connect(self):
print("Connecting to Oracle database")
def disconnect(self):
print("Disconnected from Oracle")

#another subclass
class Sybase(Myclass):
def connect(self):
print("Connecting to Sybase database")
def disconnect(self):
print("Disconnected from Sybase")
Now, it is possible to create objects to the subclasses and call the connect() method and
disconnect() methods.
First, the programmer should accept the database name from the user. It may be
‘Oracle’ or ‘Sybase’. This name should be taken in a string say ‘str’. The next step is to
connect this string into a class name using the built in function globals(). The globals()
function returns a dictionary containing current global names and global()[str] returns the
name of the class that is in ‘str’. Hence we can get the class name
as:classname=globals()[str]
Now, create an object to this class and call the methods as:
x=classname()
x.connect()
x.disconnect()
The program is given below
#abstract class works like an interface
from abc import *
class Myclass(ABC):
@abstractmethod
def connect(self):
pass OUTPUT
@abstractmethod Enter database nameOracle
def disconnect(self): Connecting to Oracle database
pass Disconnected from Oracle
#subclass Enter database nameSybase
class Oracle(Myclass): Connecting to Sybase database
def connect(self): Disconnected from Sybase
print("Connecting to Oracle database")
def disconnect(self):
print("Disconnected from Oracle")

#another subclass
class Sybase(Myclass):
def connect(self):
print("Connecting to Sybase database")
def disconnect(self):
print("Disconnected from Sybase")

class Database:
#accept database name as a string
str=input("Enter database name")
#convert string into class name
classname=globals()[str]
#create an object
x=classname()
#call methods
x.connect()
x.disconnect()
Abstract Classes vs. Interfaces
Python does not provide interface concept explicitly. It provides abstract classes
which can be used as either abstract classes or interfaces. It is the wish of the programmer to
decide when to use an abstract class and when to go for an interface.
There is a responsibility for the programmer to provide the sub classes whenever he
writes an abstract class. This means the same development team should provide the sub
classes for the abstract class. But if an interface is written, any third party vendor will take the
responsibility of providing sub classes.
In case of an interface, every time a method is called,PVM should search for the
method in the implementation classes which are installed elsewhere in the system and then
execute the method. This takes more time. But when an abstract class is written, since the
common methods are defined within the abstract class and the sub classes are generally in the
same place along with the software,PVM will not have that much overhead to execute a
method. Hence,interfaces are slow when compared to abstract classes.
Chapter 4
Exceptions
Exceptions
An exception is a run time error which can be handled by the programmer. That
means if the programmer can guess an error in the program and he can do something to
eliminate the harm caused by that error, then it is called an ‘exception’. If the programmer
can’t do anything in case of an error, then it is called an ‘error’ and not an exception.
All exceptions are represented as classes in Python. The exceptions which are already
available in Python are called ‘built-in’ exceptions. The base class for all built-in exceptions
is ‘BaseException’ class. From ‘BaseException’ class, the sub class ‘Exception’ is derived.
From ‘Exception’ class, the sub classes ‘StandardError’ and ‘Warning’ are derived.
All errors (or exceptions) are defined as sub classes of ‘StandardError’. An error
should be compulsorily handled otherwise the program will not execute. Similarly, all
warnings are derived as sub classes from ‘Warning’ class. A warning represents a caution and
even though it is not handled, the program will execute. So, warnings can be neglected but
errors cannot be neglected.
Just like the exceptions which are already available in Python language, a
programmer can also create his own exceptions called ‘user defined’ exceptions. When the
programmer wants to create his own exception class, he should derive his class from
‘Exception’ class and not from ‘BaseException’ class. The below figure shows the important
classes available in Exception hierarchy
BaseException

Exception

StandardError Warning

ArithmeticError DeprecationWarning

AssertionError RuntimeWarning

SyntaxError ImportWarning

TypeError

EOFError

RuntimeError

ImportError

NameError

BaseException

Exception Handling
The purpose of handling errors is to make the program robust. The word robust means
strong. A robust program does not terminate in the middle. Also, when there is an error in the
program, it will display an appropriate message to the user and continue execution. Designing
such programs is needed in any software development. For this purpose, the programmer
should handle the errors. When the errors can be handled, they are called exceptions.
To handle exceptions, the programmer should perform the following 3 steps
Step 1: The programmer should observe the statements in his program where there may be a
possibility of exceptions. Such statements should be written inside a ‘try’ block. A try block
looks like as follows
try:
statements
The greatness of try block is that even if some exceptions arise inside it, the program will not
be terminated. When PVM understands that there is an exception, it jumps into an ‘except’
block.
Step 2: The programmer should write the ‘except’ block where he should display the
exception details to the user. This helps the user to understand that there is some error in the
program. The programmer should also display a message regarding what can be done to
avoid this error. Except block looks like as follows
except exceptionname:
statements #these statements from handler
The statements written inside an ‘except’ block are called ‘handlers’ since they handle the
situation when the exception occurs.
Step 3: Lastly, the programmer should perform clean up actions like closing the files and
terminating any other processes which are running. The programmer should write this code in
the finally block. Finally block looks like as follows
finally:
statements
Performing the above 3 tasks is called ‘exception handling’.
#an exception handling example OUTPUT 1
try: Enter the first number2
a=int(input("Enter the first number")) Enter the second number3
b=int(input("Enter the second number")) Terminated
c=a/b
except ZeroDivisionError: OUTPUT 2
print("Division by zero happened") Enter the first number2
print("Please do not enter 0") Enter the second number0
finally: Division by zero happened
print("Terminated") Please do not enter 0
Terminated

From the preceding output, we can understand that the ‘finally’ block is executed and the file
is closed in both the cases i.e. when there is no exception and when the exception occurred.
The complete exception handling syntax is:
try:
statements
except Exception1:
handler1
except Exception2:
handler2
else:
statements
finally:
statements
The ‘try’ block contains the statements where there may be one or more exceptions. The
subsequent ‘except’ blocks handle these exceptions. When ‘Exception1’ occurs, ‘handler1’
statements are executed. When ‘Exception2’ occurs, ‘handler2’ statements are executed and
so forth. If no exception is raised, the statements inside the ‘else’ block are executed. Even if
the exception occurs or does not occur, the code inside ‘finally’ block is always executed.
Types of Exceptions
There are several exceptions available as part of Python language that are called built-
in exceptions. Similarly, the programmer can also create his own exceptions called user-
defined exceptions. Most of the exception class names end with the word ‘Error’.
Exception Class Name Description
Exception Represents any type of exception. All exceptions are sub classes of this class
ArithmeticError Represents the base class for arithmetic errors like OverflowError,
ZeroDivisionError, FloatingPointError
AssertionError Raised when an assert statement gives error
AttributeError Raised when an attribute reference or assignment fails
EOFError Raised when input() function reaches end of file condition without reading any
data
FloatingPointError Raised when a floating point operation fails
GeneratorExit Raised when generators close() method is called
IOError Raised when an input or output operation failed.
ImportError Raised when an import statement fails to find the module being imported
IndexError Raised when a sequence index is out of range
KeyError Raised when a mapping (dictionary) key is not found in the set of existing
keys
KeyboardInterrupt Raised when the user hits the interrupt key (Ctrl-C or Delete)
NameError Raised when an identifier is not found locally or globally
OverflowError Raised when the result of an arithmetic operation is too large to be represented
RuntimeError Raised when an error is detected that does not fall in any of the other
categories
SyntaxError Raised when the compiler encounters a syntax error
IndentationError Raised when indentation is not specified properly
ZeroDivisionError Raised when the denominator is zero in a division or modulus operation
The Except Block
The ‘except’ block is useful to catch an exception that is raised in the try block. When
there is an exception in the try block, then only the except block is executed. It is written in
various formats.
1. To catch the exception which is raised in the try block, we can write except block with the
Exceptionclass name as:
except Exceptionclass:
2. We can catch the exception as an object that contains some description about the
exception
except Exceptionclass as obj:
3. To catch multiple exceptions, we can write multiple catch blocks. The other way is to use
a single except block and write all the exceptions as a tuple inside parenthesis as:
except(Exceptionclass1,Exceptionclass2,……):
4. To catch any type of exception where we are not bothered about which type of exception
it is, we can write except block without mentioning any Exceptionclass name as:
except:
We can use try block without except block. When we want to use try block alone, we
need to follow it with a finally block. Since we are not using except block, it is not
possible to catch the exception
#try without except block
try: OUTPUT
x=int(input("Enter a number")) Enter a number 5
y=1/x We don't catch the exception
finally: The inverse is 0.2
print("We don't catch the exception")
print("The inverse is",y)
The assert Statement
The assert statement is useful to ensure that a given condition is True. If it is true, it raises
assertion error. The syntax is as follows: assert condition, message
If the condition is False, then the exception by the name AssertionError is raised along with
the ‘message’ written in the assert statement. If message is not given in the assert statement,
and the condition is False, and then also AssertionError is raised without message.
Program 1:
#handling AssertionError
try:
x=int(input("Enter a number between 5 and 10: "))
assert x>=5 and x<=10
OUTPUT
print("The number entered: ",x)
Enter a number between 5 and 10:
except AssertionError:
6
print("The condition is not fulfilled") The number entered: 6

OUTPUT
Program 2:
Enter a number between 5 and 10:
#handling AssertionError with a message
7
try:
The number entered: 7
x=int(input("Enter a number between 5 and 10:"))
assert x>=5 and x<=10, "Your input is not correct"
print("The number entered: ",x)
except AssertionError as obj:
print(obj)
User Defined Exceptions
Like the built in exceptions of Python, the programmer can also create his own exceptions
which are called ‘user defined exceptions’ or ‘custom exceptions’.
To create his own exceptions he has to follow the following steps:
1. Since all exceptions are classes, the programmer is supposed to create his own exception
as a class. Also, he should make his class as a subclass to the in-built ‘Exception’ class
class MyException(Exception):
def __init__(self,arg):
self.msg=arg
Here ‘MyException’ class is the subclass for ‘Exception’ class. This class has a constructor
where a variable ‘msg’ is defined. The ‘msg’ variable receives a message passed from outside
through ‘arg’.
2. The programmer can write his code; maybe it represents a group of statements or a
function. When the programmer suspects the possibility of exception, he should raise his
own exception using raise statement as: raise MyException("message")
Here, raise statement is raising MyException class object that contains the given ‘message’.
3. The programmer can insert the code inside a ‘try’ block and catch the exception using
‘except’ block as:
try:
code
except MyException as me:
print(me)
Here the object ‘me’ contains the message given in the raise statement. All these steps are
shown in below program
#creating own class as sub class to Exception class
class MyException(Exception):
def __init__(self,arg):
self.msg=arg
#write code where exception may arise
#to raise the exception, use raise statement
def check(dict):
for k,v in dict.items():
print("name={:15s} Balance={}".format(k,v))
if(v<2000.00):
raise MyException("Balance amount is less")
bank={"Raj":5000.00,"Vani":8900.50,"Ajay":1990.00,
"Naresh":3000.00}
try: OUTPUT
check(bank) name=Raj Balance=5000.0
except MyException as me: name=Vani Balance=8900.5
name=Ajay Balance=1990.0
print(me)
Balance amount is less

Logging the Exceptions


It is a good idea to store all the error messages raised by a program into a file. The file
which stores the messages, especially of errors or exceptions is called a ‘log’ file and this
technique is called ‘logging’. When we store the messages into a log file, we can open the file
and read it or take a print out of the file later. This helps the programmer to understand how
many errors are there, the names of those errors and where they are occurring in the program.
This information will enable them to pinpoint the errors and also rectify them easily. So,
logging helps in debugging the programs.
Python provides a module ‘logging’ that is useful to create a log file that can store all
error messages that may occur while executing a program
There may be different levels of error messages. For example an error that crashes
the system should be given more important than the error that merely displays a warning
message. So depending on the seriousness of the error they are classified into six levels in
‘logging; module
Level Numeric value Description
CRITICAL 50 Represents a very serious error that needs high attention
ERROR 40 Represents a serious error
WARNING 30 Represents a warning message, some caution is needed
INFO 20 Represents a message with some important information
DEBUG 10 Represents a message with debugging information
NOTSET 0 Represents that the level is not set
As we know, by default, the error messages that occur at the time of executing a
program are displayed on the user’s monitor. Only the messages which are equal to or above
the level of WARNING are displayed. That means WARNINGS, ERRORS and CRITICAL
ERRORS are displayed.
To understand different levels of logging messages, we are going to write a Python
program. In this program, first we have to create a file for logging or storing the messages.
This is done using method of logging module as:
logging.basicConfig(filename='mylog.txt',level=logging.ERROR)
Here the log file name is given as ‘mylog.txt’. The level is set to ERROR. Hence the
messages whose level will be at ERROR or above (ERROR or CRITICAL) will only be
stored into the log file. Once this is done we can add the messages to the ‘mylog.txt’ file as:
logging.methodname("message")

The method names can be critical(), error(), warning(), info() and debug(). For example:
logging.critical("System crash")
Now this error message is stored into the log file that is ‘mylog.txt’.
Example 1:
#understanding logging of error messages
import logging
#store messages into mylog.txt file
logging.basicConfig(filename='mylog.txt',level=logging.ERROR)
#these messages are stored into the file.
logging.error("There is an error in the program")
logging.critical("There is a problem in the design")
#these messages are not stored
logging.warning("The project is going slow")
logging.info("You are a junior programmer")
logging.debug("Line no. 10 contains syntax error")
When the above program is executed, we can see a file created by the name ‘mylog.txt’ in
our current directory. Open the file to see the following message:
ERROR:root:There is an error in the program
CRITICAL:root:There is a problem in the design
Example 2:
import logging
logging.basicConfig(filename='log.txt',level=logging.ERROR)
try:
a=int(input("Enter the first number:")) OUTPUT
b=int(input("Enter the second number:")) Enter the first number:10
c=a/b Enter the second number:20
The result of division: 0.5
except Exception as e:
>>>
logging.exception(e)
Enter the first number:10
else:
Enter the second number:0
print("The result of division: ",c)
>>>
Enter the first number:10
Enter the second number:ab

Please observe that the program is executed 3 times with different inputs. First time the
values supplied are 10 and 20. With these values the program executed well and there are no
exceptions. Second time the values supplied is 10 and 0. In this case there is possibility for
ZeroDivisionError. The message related to this exception will be stored into our log file. In
the third time execution, the values entered are 10 and ab. In this case there is possibility for
ValueError. The message of this exception will also be added to our log file. Now we can
open the log file and see the following messages:
ERROR:root:division by zero
Traceback (most recent call last):
File "C:/jecci/exception/16.py", line 6, in <module>
c=a/b
ZeroDivisionError: division by zero
ERROR:root:invalid literal for int() with base 10: 'ab'
Traceback (most recent call last):
File "C:/jecci/exception/16.py", line 5, in <module>
b=int(input("Enter the second number:"))
ValueError: invalid literal for int() with base 10: 'ab'
Chapter 5
Regular Expressions
A regular expression is a string that contains special symbols and characters to find
and extract the information needed by us from the given data. Regular expression helps us to
search information, match, find and split information as per our requirement. A regular
expression is also called simply regex.
Python provides ‘re’module that stands for regular expressions. This module contains
methods like compile(), search(), match(), findall(), split(), etc which are used in finding the
information in the available data. So when we write a regular expression we should import
module as: import re
Regular expressions are nothing but strings containing characters and special symbols. A
simple regular expression may look like this: reg=r‘m\w\w’
In the above line, the string is prefixed with ‘r’ to represent that it is a raw string. Generally
we write regular expressions as raw strings. When we write a normal string as:
str= “This is a normal\string”
Now, print(str) will display the preceding string in two lines as:
This is a normal
string
Thus the ‘\n’ character is interpreted as new line in the normal string by the Python
interpreter and hence the string is broken there and shown in the new line. In regular
expressions when ‘\n’ is used, it does not mean to throw the string into new line. For this
purpose, we should take this as a ‘raw’ string. This is done by prefixing ‘r’ before the string.
str=r ‘This is raw\nstring’
When we display this string using print(str). The output will be:
This is raw\nstring
So, the normal meaning of ‘\n’ is escaped and it is no more an escape character in the
preceding example. Since ‘\n’ is not an escape character, it is interpreted as a character with
different meaning in the regular expression by the Python interpreter. Similarly, the
characters like ‘\t’, ‘\w’, ‘\c’, etc should be interpreted as special characters in the regular
expressions and hence the expressions should be written as raw strings. If we don’t want to
write the regular expressions as raw strings, then the alternative is to use another backslash
before such characters. For example,
reg=r‘m\w\w’ #as raw string
reg=‘m\\w\\w’ #as normal string
Consider the regular expression:
reg=r‘m\w\w’
This expression is written in single quotes to represent that it is a string. The first character
‘m’ represents that the words starting with ‘m’ should be matched. The next character ‘\w’
represents any one character in A to Z, a to z, and 0 to 9. Since we used two ‘\w’ characters,
they represent any two characters after ‘m’. So, this regular expression represents words or
strings having three characters and with ‘m’ as first characters. The next two characters can
be any alphanumeric.
Now we developed our first regular expression. The next step is to compile this
expression using compile () method of ‘re’ module as:
prog=re.compile(r‘m\w\w’)
Now prog represents an object that contains the regular expression. The next step is to run
this expression on a string ‘str’ using search() method or match() methods as:
str='cat mat bat rat'
#expression will act
result=prog.search(str)
The result is stored in ‘result’ object and we can display it by calling the group() method on
the object as:
print(result.group())
This is how a regular expression is created and used.
Example 1:
import re
prog=re.compile(r'm\w\w') OUTPUT
str='cat mat bat rat' mat
result=prog.search(str)
print(result.group())
Example 2:
import re
prog=re.compile(r'm\w\w') OUTPUT
str='Operating system format' mat
result=prog.search(str)
print(result.group())
Instead of compiling the first regular expression and then running the next one, we can use a
single step to compile and run all the regular expression as:
result=re.search(r'm\w\w')
The above code is equivalent to:
prog=re.compile(r'm\w\w')
result=prog.search(str)
So, the general form of writing regular expressions is:
result=re.search(‘expression’,‘string’)
Example:
import re
str='man sun mop run' OUTPUT
result=re.search(r'm\w\w',str) man
if result:
print(result.group())
Observe the output of above program. The search () method searches for the strings
according to the regular expression and returns only the first string. This is the reason that
even though there are two strings ‘man’ and ‘mop’, it returned only the first one. This string
can be extracted using the group () method. In case, the search () method could not find any
strings matching the regular expression, and then it returns None. So, to display the result, we
can use either of the statements given here:
if result is not None:
print(result.group())
if result:
print(result.group())
Suppose, we want to get all the strings that match the pattern mentioned in the regular
expression, we should use findall () method instead of search () method. The findall ()
method returns all resultant strings into a list.
import re
str='man sun mop run' OUTPUT
['man', 'mop']
result=re.findall(r'm\w\w',str)
print(result)
There is another method by the name match() that returns the resultant string only if it is
found in the beginning of the string. The match() method will give None if the string is not in
the beginning.
import re
str='sun mop run man' OUTPUT
result=re.match(r'm\w\w',str) None
print(result)
There is another method by the name split() that splits the given string into pieces according
to the regular expression and returns the pieces as elements of a list. Suppose we write a
regular expression as: re.split(r‘\W+’,str)
Observe the regular expression ‘\W’. This is capital ‘W’ which is reverse to the small ‘w’.
‘w’ represents any one alphanumeric character, i.e. A-Z, a-z, 0-9. But ‘W’ represents any
character that is not alpha numeric. So, the work of this regular expression is to split the
string ‘str’ at the places where there is no alpha numeric character. The ‘+’ after W represents
to match 1 or more occurrences indicated by ‘w’. The result is that the string will be split into
pieces where 1 or more non alpha numeric characters are found.
import re
OUTPUT
str='This; is the: "Core" Python\'s book' ['This', 'is', 'the', 'Core', 'Python', 's', 'book']
result=re.split(r'\W+',str)
print(result)
Sometimes, regular expressions can also be used to find a string and then replace it with a
new string. For this purpose, we should use sub() method of ‘re’ module. The format of this
method is: sub(regular expression,newstring,string)
Example:
import re
str='Kumbhamela will be conducted at Ahmedabad in India'
result=re.sub(r'Ahmedabad','Allahabad',str)
print(result)
OUTPUT
Kumbhamela will be conducted at Allahabad in India

So, regular expressions are used o perform the following important operations:
1. Matching strings
2. Searching for strings
3. Finding all strings
4. Splitting a string into pieces
5. Replacing strings
Sequence characters in Regular Expressions
Sequence characters match only one character in the string.
Character Description
\d Represents any digit (0 to 9)
\D Represents any non digit
\s Represents white space. Example: \t\n\r
\S Represents non-white space characters
\w Represents any alphanumeric
\W Represents non-alphanumeric
\b Represents a space around words
\A Matches only at start of the string
\Z Matches only at end of the string
Each of these sequence characters represents a single character matched in the string. For
example ‘\w’ indicates any one alphanumeric character. Suppose we write it as [\w]*. Here *
represents 0 or more repetitions.
import re
str='an apple a day keeps the doctor away' OUTPUT
result=re.findall(r'a[\w]*',str) ['an', 'apple', 'a', 'ay', 'away']
print(result)
The above output contains ‘ay’ which is not a word. This ‘ay’ is part of the word ‘away’. So
it is displaying both ‘ay’ and ‘away’ as they are starting with ‘a’. We do not want like this.
We want only the words starting with ‘a’. Since a word will have a space in the beginning or
ending, we can use \b before and after the words in the regular expression.
import re
OUTPUT
str='an apple a day keeps the doctor away'
['an', 'apple', 'a', 'away']
result=re.findall(r'\ba[\w]*\b',str)
print(result)
To retrieve all the words starting with a numeric digit like 0,1,2 or 9, \d is used.
import re
str='The meeting will be conducted on 1st and 21st of every month'
result=re.findall(r'\d[\w]*',str) OUTPUT
print (result) ['1st', '21st']

If we want to retrieve all words having 5 characters length, it can be written as


r'\b\w{5}\b',str. The character \b represents a space.
import re
str='one two three four five six seven 8 9 10' OUTPUT
result=re.findall(r'\b\w{5}\b',str) ['three', 'seven']
print(result)
Instead of findall() method, if we use the search()method, it will return the first occurrence of
the result only.
import re
str='one two three four five six seven 8 9 10' OUTPUT
result=re.search(r'\b\w{5}\b',str) three
print(result)
If we want to find all words which are at least 4 characters long:
import re
OUTPUT
str='one two three four five six seven 8 9 10'
['three', 'four', 'five', 'seven']
result=re.findall(r'\b\w{4,}\b',str)
print(result)
To retrieve all words with 3 to 5 characters length:
import re
str='one two three four five six seven 8 9 10'
result=re.findall(r'\b\w{3,5}\b',str) OUTPUT
print(result) ['one', 'two', 'three', 'four', 'five', 'six', 'seven']
To retrieve only single digits from a string:
import re
str='one two three four five six seven 8 9 10' OUTPUT
result=re.findall(r'\b\d\b',str) ['8', '9']
print(result)
Quantifiers in Regular Expressions
In regular expressions, some characters represent more than one character to be matched in
the string. Such characters are called quantifiers. For example if we write ‘+’ it represents 1
or more repetitions of the preceding character.
Character Description
+ 1 or more repetitions of the preceding regexp
* 0 or more repetitions of the preceding regexp
? 0 or 1 repetitions of the preceding regexp
{m} Exactly m occurrences
{m,n} From m to n. m defaults to 0. n to infinity
To retrieve the phone number of a person from a string using the regular expression is:
import re
str='NageswaraRao: 9706612345' OUTPUT
result=re.search(r'\d+',str) 9706612345
print(result.group())
To retrieve the person’s name and not his phone number, instead of writing \d we use \D. \D
represents all characters except numeric characters
import re
str='NageswaraRao: 9706612345' OUTPUT
result=re.search(r'\D+',str) NageswaraRao:
print(result.group())
The special character ‘+’ represents 1 or more repetitions. Similarly ‘*’ represents 0 or more
repetitions. Suppose we want to write a regular expression that finds all words starting with
either ‘an’ or ‘ak’, then:
import re
str='anilakhilanantharunarathiarundathiabhijith' OUTPUT
result=re.findall(r'a[nk][\w]*',str) ['anil', 'akhil', 'ananth']
print(result)
{m,n} indicates m to n occurrences.
import re
str="Vijay 20 1-5-2001, Rohith 21 22-10-1990, Sitha 22 15-09-2000"
result=re.findall(r'\d{2}-\d{2}-\d{4}',str)
OUTPUT
print(result) ['22-10-1990', '15-09-2000']
To retrieve either 1 or 2 digits in the date or moth:
import re
str="Vijay 20 1-5-2001, Rohith 21 22-10-1990, Sitha 22 15-09-2000"
result=re.findall(r'\d{1,2}-\d{1,2}-\d{4}',str)
print(result)
OUTPUT
['1-5-2001', '22-10-1990', '15-09-2000']
Special Characters in Regular Expressions

Character Description
\ Escape special character nature
. Matches any character except new line
^ Matches beginning of a string
$ Matches ending of a string
[…] Denotes a set of possible characters
[^…] Matches every character except the ones inside brackets
(…) Matches the regular expression inside the parenthesis and the result can be
captured
R|S Matches either regex R or regex S
To create a regular expression to search whether a given string is starting with ‘He’ or not:
import re
str="Hello World"
result=re.search(r"^He",str)
if result: OUTPUT
print("String starts with He") String starts with He
else:
print("String does not start with He")
To know, whether a string is ending with a word, we can use $ symbol.
import re
str="Hello World"
result=re.search(r"World$",str)
if result: OUTPUT
print("String ends with World") String ends with World
else:
print("String does not end with World")
If we use the expression with a small ‘w’ as r“world$”, then we will end up with wrong
output:
import re
str="Hello World"
result=re.search(r"world$",str)
OUTPUT
if result:
String does not end with world
print("String ends with world")
else:
print("String does not end with world")
We can specify a case insensitive matching of strings with the help of IGNORECASE
constant of ‘re’ module
import re
str="Hello World"
result=re.search(r"world$",str,re.IGNORECASE)
if result:
print("String ends with world") OUTPUT
else: String ends with world
print("String does not end with world")
The square brackets [] in regular expressions represent a set of characters. For example, if we
write [ABC], it represents any one character A or B or C. A hyphen (-) represents a range of
characters. For example, [A-Z] represents any single character from the range of capital
letters A to Z. the regular expression ‘[A-Z][a-z]*’ means the first letter should be any capital
letter. Then the next letter should be a small letter. The ‘*’ symbol represents 0 or more
repetitions of small letters.
import re
str="Rahul got 75 marks, Vijay got 55 marks, whereas Subbu got 98
marks"
marks=re.findall('\d{2}',str) OUTPUT
['75', '55', '98']
print(marks)
['Rahul', 'Vijay', 'Subbu']
names=re.findall('[A-Z][a-z]*',str)
print(names)
The pipe symbol (|) represents ‘or’. For example if we write am | pm, it finds the strings
which are either ‘am’ or ‘pm’
import re
str="The meeting may be at 8am or 9am or 4pm or 5pm"
result=re.findall(r'\dam|\dpm',str) OUTPUT
print(result) ['8am', '9am', '4pm', '5pm']

Chapter 6
Threads
Creating Threads in Python
Python provides ‘Thread’ class of threading module that is useful to
create threads. To create our own thread, we are supposed to create an object of
Thread class. The following are the different ways of creating our own threads
in Python:
1. Creating a thread without using a class
2. Creating a thread by creating a sub class to Thread class
3. Creating a thread without creating sub class to Thread class
Creating a thread without using a class
The purpose of a thread is to execute a group of statements like a
function. So, we can create a thread by creating an object of Thread class and
pass the function name as target for the thread as:
t=Thread(target=functionname, [args=(arg1,arg2,….)])
Here, we are creating the Thread class object ‘t’ that represents our
thread. The ‘target’ represents the function on which the thread will act. ‘args’
represents a tuple of arguments which are passed to the function. Once the
thread is created like this, it should be started by calling the start () method as:
t.start()
Then the thread ‘t’ will jump into the target function and executes the code
inside that function.
from threading import * OUTPUT
def display(): Hello I am running
print("Hello I am running") Hello I am running
Hello I am running
for i in range(5):
Hello I am running
t=Thread(target=display) Hello I am running
t.start()

We can slightly improve the previous program such that we can pass arguments
or data to the display () function.
OUTPUT
from threading import * Hello
def display(str): Hello
print(str) Hello
Hello
for i in range(5):
Hello
t=Thread(target=display,args=("Hello",))
t.start()
Note the comma (,) after the argument ‘Hello’ mentioned in the tuple. We
should remember that when a single element is specified in a tuple, a comma is
needed after that element
Creating a Thread by creating a subclass to Thread Class
Since ‘Thread’ class is already created by the Python people in the threading
module, we can make our class as a sub class to ‘Thread’ class so that we can
inherit the functionality of the Thread class. This can be done by writing the
following statement:
class MyThread(Thread)
Here ‘MyThread’ represents the sub class of ‘Thread’ class. Now, any
methods of Thread class are also available to our sub class. The ‘Thread’ class
has the run () method which is also available to our sub class i.e. to ‘MyThread’
class. The specialty of the run() method is that every thread will run this method
when it is started. So, by overriding the run () method, we can make the threads
run our own run() method. Overriding a method – means writing our own
method with the same name as the run() method of the super class.

The next step is to create the object of the ‘MyThread’ class which
contains a copy of the super class, i.e. the ‘Thread’ class.

t1=MyThread

Here, t1 is the object of ‘MyThread’ class which represents our thread. Now, we
can run this thread by calling the start() method as: t1.start(). Now the
thread will jump into the run() method of ‘MyThread’ class and executes the
code available inside it.

Many times it is better to wait for the completion of the thread by calling the
join() method on the thread as:t1.join(). This will wait till the thread completely
executes the run() method. A thread will terminate automatically when it comes
out of the run() method.

from threading import Thread


OUTPUT
class MyThread(Thread): 1
def run(self): 2
for i in range(1,6): 3
4
print(i)
5
t1=MyThread()
t1.start()
t1.join()
Sometimes threads may want to act on the instance variables. For this purpose,
we have to write a constructor inside our class as:
def __init__(self,str):
self.str=str
This constructor accepts a string ‘str’ and initializes an instance variable
‘self.str’ with that string. But there is a restriction here. When we write a
constructor, it will override the constructor of the super class i.e. ‘Thread’ class.
So, to retain the functionality of the ‘Thread’ class, we have to call its
constructor from the sub class constructor as:
def __init__(self,str):
Thread.__init__(self)#call Thread class constructor
self.str=str

Exmaple:
from threading import Thread OUTPUT
class MyThread(Thread): Hello
def __init__(self,str):
Thread.__init__(self)
self.str=str
def run(self):
print(self.str)
t1=MyThread("Hello")
t1.start()
t1.join()

Creating a Thread without creating Sub Class to Thread Class

We can create an independent class say ‘MyThread’ that does not inherit from
‘Thread’ class. Then we can create an object ‘obj’ to ‘MyThread’ class as:
obj=MyThread(“Hello”)

The next step is to create a thread by creating an object to ‘Thread’ class and
specifying the method of the ‘MyThread’ class as its target as:
t1=Thread(target=obj.display,args=(1,2))

Here ‘t1’ is our thread which is created as an object of ‘Thread’ class. ‘target’
represents the display () method of ‘MyThread’ object ‘obj’. ‘args’ represents a
tuple of values passed to the method. When the thread ‘t1’ is started, it will
execute the display() method of ‘MyThread’ class.

from threading import *


OUTPUT
class MyThread: Hello
def __init__(self,str): The args are: 1 2
self.str=str
def display(self,x,y):
print(self.str)
print("The args are: ",x,y)
obj=MyThread("Hello")
t1=Thread(target=obj.display,args=(1,2))
t1.start()

Thread Class Methods

Method Description
t.start() Starts the thread. It arranges for the object’s run() method to be
invoked in a separate thread of control
t.join([timeout]) Waits until the thread terminates or timeout occurs. ‘timeout’ is a
floating point number specifying a timeout for the operation in
seconds (or fraction of seconds)
t.is_alive() Returns True if the thread is alive in memory and False otherwise.
A thread is alive from the moment the start() method returns until
its run() method terminates
t.setName(name) Gives a name to the thread
t.getName() Returns name of the thread
t.name This is a property that represents the thread’s name
t.setDaemon(flag) Makes a thread a daemon thread if the flag is True
t.isDaemon() Returns True if the thread is a daemon thread, otherwise False
t.daemon This is a property that takes either True or False to set the thread as
daemon or not

Thread Synchronization

When a thread is already acting on an object, preventing any other thread


from acting on the same object is called ‘thread synchronization’ or ‘thread
safe’. The object on which the threads are synchronized is called ‘synchronized
object’ or ‘mutex’ (mutually exclusive lock). Thread synchronization is
recommended when multiple threads are acting on the same object
simultaneously. Thread synchronization is done using the following techniques:

1. Using locks
2. Using semaphores
Locks

Locks can be used to lock the object on which the thread is acting. When
a thread enters the object, it locks the object and after the execution is
completed, it will unlock the object and comes out of it. It is a like a room with
only one door. A person has entered the room and locked it from behind. The
second person who wants to enter the room should wait till the first person
comes out. In this way, a thread also locks the object after entering it. Then the
next thread cannot enter it till the first thread comes out, this means the object is
locked mutually on threads. So, this locked object is called ‘mutex’
t2

t1
wait till t1 comes out

t1 entered the obj mutex

We can create a lock by creating an object of Lock class as:


l=Lock()
To lock the current object, we should use acquire() method as:
l.acquire()
To unlock or release the object, we can use release() method as:
l.release()

Example:
#thread synchronization using locks
from threading import *
from time import *

class Railway:
#constructor
def __init__(self,available):
self.available=available
#create a lock object
self.l=Lock()

# method
def reserve(self,wanted):
#lock the current object
self.l.acquire()
#display no.of available seats
print("Available number of seats= ",self.available)
if(self.available>=wanted):
#find the thread name
name=current_thread().getName()
print("%d seat alloted for %s"%(wanted,name))
#decrease the no.of available seats
self.available-=wanted
else:
print("sorry, no seats available")
self.l.release()
#create instance
obj=Railway(1)
#create two threads
t1=Thread(target=obj.reserve,args=(1,))
t2=Thread(target=obj.reserve,args=(1,))
#give names to the threads
t1.setName('First Person')
t2.setName('Second Person') OUTPUT
#run the threads Available number of seats= 1
1 seat alloted for First Person
t1.start()
Available number of seats= 0
t2.start()
sorry, no seats available

Semaphore

A Semaphore is an object that provides synchronization based on a


counter. A semaphore is created as an object of Semaphore class as:
l=Semaphore(countervalue) #here the counter value by default is 1

If the ‘countervalue’ is not given, the default value of the counter will be
1. When the acquire () method is called, the counter gets decremented by 1 and
when release () method is called, it is incremented by 1. These methods are used
in the following format:

l.acquire() #make counter 0 and then lock


#code that is locked by semaphore
l.release()# counter is 0 so unlock and make counter 1

Communication between Threads


In some cases, two or more threads should communicate with each other.
For example, a Consumer thread is waiting for a Producer to produce the data
(goods). When the Producer thread completes production of data, then the
Consumer thread should take that data and use it. The Consumer thread does not
know how much time it takes for the Producer to complete the production. At
the producer side, we will take a list ‘lst’ to which the items will be added. The
Producer produces 10 items and each time an item is produced, we can display a
message ‘Item produced’
for i in range(1,11): #repeat from 1 to 10
lst.append(i) #add item to list
sleep(1) #every item may take some time
print("Item Produced")
In the Producer class, we take another Boolean type variable ‘dataprodover’ and
initialize it to False. The idea is to make the ‘dataprodover’ variable True when
the production of all the 10 items is completed.
dataprodover=True
This will inform the Consumer that the production is over and hence you can
utilize the data available in the list ‘lst’. When the Producer is busy producing
the data, now and then the Consumer will check if ‘dataprodover’ is True or not.
If ‘dataprodover’ is True, then Consumer takes the data from the list and uses it.
If the ‘dataprodover’ shows False, the Consumer will sleep for some time and
then again checks the ‘dataprodover’ to know whether it is True or not. The way
to implement this logic at Consumer side is:
#sleep for 100 ms as long as dataprodover is False
#prod represents Producer instance that contains dataprodover
while prod.dataprodover==False:
sleep(0.1)
In this way, the Producer and consumer can communicate with each other

#thread communication
from threading import *
from time import *
#create Producer class
class Producer:
def __init__(self):
self.lst=[]
self.dataprodover=False
def produce(self):
#create 1 to 10 items and add to the list
for i in range(1,11): #repeat from 1 to 10
self.lst.append(i) #add item to list
sleep(1) #every item may take some time
print("Item Produced")
#inform the onsumer that the production is completed
self.dataprodover=True
#Consumer class
class Consumer:
def __init__(self,prod):
self.prod=prod
def consume(self):
#sleep for 100 ms as long as dataprodover is False
#prod represents Producer instance that contains
dataprodover
whileself.prod.dataprodover==False:
OUTPUT
sleep(0.1)
Item Produced
#display the contents
Item Produced
print(self.prod.lst)
Item Produced
#create Producer object
Item Produced
p=Producer() Item Produced
#create Consumer object Item Produced
c=Consumer(p) Item Produced
#create threads Item Produced
t1=Thread(target=p.produce) Item Produced
t2=Thread(target=c.consume) Item Produced
#run threads [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
t1.start()
t2.start()

Thread Communication using notify() and wait() Methods


The condition class of threading module is useful to improve the speed of
communication between the threads. The Condition class object is called
condition variable and is created as: cv=Condition()
This class contains methods which are used in thread communication.
The notify() method is useful to immediately inform the Consumer thread
that the data production is completed. The notify_all() method is useful to
inform all waiting Consumer threads at once that the production is over. These
methods are used by the Producer thread.
The Consumer thread need not check if the data production is over or not
through a boolean type variable like ‘dataprodover’. The Consumer can simply
wait till it gets the notification from the notify() or notify_all() methods.
Consumer can wait using the wait() method which terminates when Producer
invokes notify() or notify_all() methods. The wait() method is written in the
form as follows: cv.wait(timeout=0)
This will wait till the notification is received. But once the notification is
received, there will not be any delay in receiving the product. So this form of
the wait() method will not waste even a single milliseconds to receive the data
after the actual production is completed. Thus, the notify(), notify_all() and
wait() methods provide efficient means of communication between threads.
These methods should be used in between the acquire() and release() methods
which are useful to lock and unlock the Condition variable.

from threading import *


from time import *
class Producer:
def __init__(self):
self.lst=[]
self.cv=Condition()
def produce(self):
self.cv.acquire() #lock the condition object
for i in range(1,11):
self.lst.append(i)
sleep(1)
print("Item Produced")
#inform the consumer that production is completed
self.cv.notify()
#release the lock
self.cv.release()
class Consumer:
def __init__(self,prod):
self.prod=prod
def consume(self):
#lock condition object
self.prod.cv.acquire()
#wait for 0 seconds after the production
self.prod.cv.wait(timeout=0)
#release the lock
self.prod.cv.release()
#display the contents of the list OUTPUT
print(self.prod.lst) Item Produced
Item Produced
#create Producer object
Item Produced
p=Producer() Item Produced
#create Consumer object Item Produced
c=Consumer(p) Item Produced
#create threads Item Produced
Item Produced
t1=Thread(target=p.produce) Item Produced
t2=Thread(target=c.consume) Item Produced
#run the threads [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
t1.start()
t2.start()

Daemon Threads
Sometimes, we need threads to run continuously in memory. For example, an
Internet server that runs continuously and caters to the needs of the client.
Garbage collector runs continuously and deletes the unused variables and
objects while a Python program is being executed. Such threads which run
continuously are called ‘daemon’ threads. Generally daemon threads are used to
perform some background tasks. For example, a daemon threads may send data
to the printer in the background while the user is working with other
applications. To make a threads a daemon thread, we can use the
setDaemon(True) method or assign True to daemon property.
In below program, we are creating a function display() that displays a
number once in a second, starting from 1 to 5. This function is executed by
normal thread‘t’. Then we create another function display_time() that displays
system date and time once in every 2 seconds. This function is executed by
another thread‘d’ which we make daemon by writing: d.daemon=True. When
both the threads are started, the results are shown in the output at the end of the
program.
from threading import *
from time import *
def display(): OUTPUT
for i in range(5): Normal thread: 1
print("Normal thread: ",i+1) Daemon thread: Wed Feb 23 15:34:48 2022
sleep(1) Normal thread: 2
def display_time(): Daemon thread: Wed Feb 23 15:34:50 2022
while True: Normal thread: 3
print("Daemon thread: ",ctime()) Normal thread: 4
sleep(2) Daemon thread: Wed Feb 23 15:34:52 2022
t=Thread(target=display)
Normal thread: 5
t.start()
d=Thread(target=display_time)
d.daemon=True
d.start()

The above program terminates when the normal thread ‘t’ after displaying the
number 5 is terminated. The reason is that when the normal threads are
terminated, the main thread will also terminate and thus the program is also
terminated. So, the daemon thread is also terminated in this program. To make
the daemon thread not to terminate, we have to ask the PVM to wait till the
daemon thread completely executes. This is done by using the join() method as:
d.join()

from threading import * OUTPUT


from time import * Normal thread: 1
def display(): Daemon thread: Wed Feb 23 15:40:25 2022
for i in range(5): Normal thread: 2
print("Normal thread: ",i+1) Daemon thread: Wed Feb 23 15:40:27 2022
sleep(1) Normal thread: 3
def display_time(): Normal thread: 4
while True: Daemon thread: Wed Feb 23 15:40:30 2022
print("Daemon thread: ",ctime()) Normal thread: 5
sleep(2) Daemon thread: Wed Feb 23 15:40:32 2022
t=Thread(target=display)
Daemon thread: Wed Feb 23 15:40:34 2022
t.start()
Daemon thread: Wed Feb 23 15:40:36 2022
d=Thread(target=display_time)
Daemon thread: Wed Feb 23 15:40:38 2022
d.daemon=True
……
d.start()
d.join()

You might also like