Oops Concepts in Python

Download as pdf or txt
Download as pdf or txt
You are on page 1of 48

keyboard_arrow_down 13.

Object Oriented Programming concepts


Object Oriented Programming (OOP) is a programming paradigm that allows abstraction
through the concept of interacting entities. This programming works contradictory to
conventional model and is procedural, in which programs are organized as a sequence of
commands or statements to perform.

We can think an object as an entity that resides in memory, has a state and it's able to perform
some actions.

More formally objects are entities that represent instances of a general abstract concept called
class. In Python , "attributes" are the variables defining an object state and the possible actions
are called "methods".

In Python, everything is an object also classes and functions.

Each class represents meaningful view of the objects that are instances of this class, without
going into too much detail or giving others access to the inner workings of the objects i.e. how it
works internally. The class contains instance variables, also known as data members as well as
some methods for some purpose also known as member functions. Object of the class can
access this methods to achieve something meaningful.

keyboard_arrow_down 13.1 Goals of object oriented design


Robustness

1) Our program should run properly even with unexpected data

2) We need to write in such a way that it handles complex programs and it can handle with
unexpected data.

Every developer develop application in such a way that an application will give correct output in
all the scenarios. Our application will be robust, that is, it should capable enough for handling
unexpected data that is not predefined. For example, if a program is expecting a string and
instead it will receive negative integer, then the program should be able to recover and handle
this type of error gracefully.

Writing software programs is a style of programming that dealing with surprising and
unforeseen activities. It expects code to deal with these types of terminations and expectations
and run smmothly.

Adaptability
1) Software program or application will grow over a life time

2) The application should build such a way that is should run with different versions, different
generations, different hardware as well.

Software program or application should have the option to advance after some time in light of
changing conditions. Along these lines, this is significant objective of value in programming that
flexibility ought to be accomplished. Program should be build in such a way that it will run with
minor change on various hardware and os platforms.

Reusability

Building from reusable applications to avoid making entire new application just like rebuilding
entire wheel.

Software should be reusable, that is, the same code should be reusable as a component for
different applications. Developing quality software application can be an expensive and if the
software is designed in a way that makes it easily reusable in future applications, it will helpful
in productivity and also can reduce cost for new application.

keyboard_arrow_down 13.2 How to define classes


keyboard_arrow_down Creating a class
Suppose we want to create a class, named Person, as a prototype, a sort of template for any
number of 'Person' objects (instances).

The following python syntax defines a class:

class ClassName(base_classes):
statements

Class names should always be uppercase (it's a naming convention).

Say we need to model a Person as:

Name
Surname
Age
class Person:
pass

obj1 = Person()
obj1.name = "Ram"
obj1.surname = "Sham"
obj1.year_of_birth = 1958
obj1

print(obj1)
print("%s %s was born in %d." %
(obj1.name, obj1.surname, obj1.year_of_birth))

pass is generally used where code will eventually run without any errors.

class Person:
pass

The following example defines an empty class (i.e. the class doesn't have a state) called Person
then creates a Person instance called john_doe and adds three attributes to john_doe. We see
that we can access objects attributes using the "dot" operator.

This isn't a recommended style because classes should describe homogeneous entities. A way
to do so is the following:

class Person:
def __init__(self, name, surname, year_of_birth):
self.name = name
self.surname = surname
self.year_of_birth = year_of_birth

__init__(self, ...)

Is a special Python method that is automatically called after an object construction. Its purpose
is to initialize every object state. The first argument (by convention) self is automatically passed
either and refers to the object itself.

In the preceding example, __init__ adds three attributes to every object that is instantiated. So
the class is actually describing each object's state.

Attributes created in __init__ method are called instance attributes, so name,surname and
year_of_birth are called instance attributes.
class Person:
# Here we can declare class attributes
company = "xyz"

def __init__(self, name, surname, year_of_birth):


self.name = name
self.surname = surname
self.year_of_birth = year_of_birth
def show(self):
print(self.name,self.surname,self.company)

obj = Person("Raja", "Ram", 1996)

obj.company

type(obj)

id(obj)

Start coding or generate with AI.

obj.show()

obj.surname

print(obj)

Here comapny is called class variables and we can use class variables if it will not change for
entire class and their methods and functions. It should be same for entire class and every
instance. We can use instance attributes which will vary from one instance to another instances
of the class.

We cannot directly manipulate any class rather we need to create an instance of the class:

obj = Person("Ramesh", "Babu", 1958)


print(obj)
print("%s %s was born in %d." % (obj.name, obj.surname, obj.year_of_birth))

# Can access class attribute company in a similar way


obj.company

We have just created an instance of the Person class, bound to the variable obj .
# Another example
class Student:

def __init__(self,first_name,last_name,age,class_,section):
self.first_name = first_name
last_name = last_name
self.age = age
self.class_ = class_
self.section = section
print(last_name)
def show(self):
print(self.first_name,self.last_name)

student1 = Student("Ram","Kumar",17,10,"A")

student1.show()

student2 = Student("Shyam","Sharma",16,9,"C")

print("Details of first student : Name = {0}, Age= {1} , class_section = {2}".


format(student1.first_name+ " " +student1.last_name, student1.age, str(student1.cla

print("Details of second student : Name = {0}, Age= {1} , class_section = {2}".


format(student2.first_name+ " " +student2.last_name, student2.age, str(student2.cla

keyboard_arrow_down Methods
class Person:
def __init__(a, name, surname, year_of_birth):
a.name = name
a.surname = surname
a.year_of_birth = year_of_birth

def age(a, current_year):


return current_year - a.year_of_birth

def __str__(a):
return "%s %s was born in %d ." % (a.name, a.surname, a.year_of_birth)

alec = Person("Alec", "Baldwin", 1958)


print(alec)
print(alec.age(2014))

We defined two more methods age and __str__ . The latter is once again a special method that
is called by Python when the object has to be represented as a string (e.g. when has to be
printed). If the __str__ method isn't defined the print command shows the type of object and
its address in memory. We can see that in order to call a method we use the same syntax for
attributes (instance_name.instance _method).

.init() and .str() methods are called dunder methods. We can call as dunder methods beacause
they begin and end with double underscores.
#Let's look at another database example for class methods
# For more detail about database, we will look in future classes.
import sqlite3

class DataBaseOperations:

def __init__(self,databasename):

self.databasename = databasename

def createDatabase(self):
try:
conn = sqlite3.connect(self.databasename)
except ConnectionError:
raise ConnectionError
return conn

def createTable(self,tablename,dictionaryOfcolumnNamesAndcolumnDatatypes):
try:
conn = self.createDatabase()
c = conn.cursor()
for key in dictionaryOfcolumnNamesAndcolumnDatatypes.keys():
datatype = dictionaryOfcolumnNamesAndcolumnDatatypes[key]
try:
conn.execute(
'ALTER TABLE {tableName} ADD COLUMN "{column_name}" {dataType}'.f

except:
conn.execute('CREATE TABLE {tableName} ({column_name} {dataType})'.fo

print("Table {0} created in database {1}".format(tablename,self.databasename


self.closeDbConnection(conn)
print("Connection to database closed!!")
except Exception as e:
conn.rollback()
self.closeDbConnection(conn)
print("Connection to database closed!!")

print("Exception occured: " + str(e))

def insertIntoTable(self,tablename, listOfvaluesToInsert):


try:
conn = self.createDatabase()
conn.execute('INSERT INTO {tablename} values ({values})'.format(tablename =
conn.commit()
print("Values Inserted Successfully!!!")
self.closeDbConnection(conn)
print("Connection to database closed!!")
except Exception as e:
conn.rollback()
self.closeDbConnection(conn)
print("Connection to database closed!!")
print("Error occured: " + str(e))
# self.closeDbconnection()

def selectFromTable(self,tablename):

try:
conn = self.createDatabase()
c = conn.cursor()
c.execute("SELECT * FROM {table}".format(table=tablename))
print("values in table : " ,c.fetchall())
self.closeDbConnection(conn)
print("Connection to database closed!!")

except Exception as e:
self.closeDbConnection(conn)
print("Connection to database closed!!")
print("Error occured: " + str(e))

def closeDbConnection(self,connection):

connection.close()

#creating an object of class databaseOperations


db = DataBaseOperations("test1")

#creating database
db.createDatabase()

tableDetails = {"studentId" : "INTEGER", "studentRoll" : "INTEGER", "studentMarks" : "FLO

db.createTable("table1",tableDetails)

valuesToisnert= ('1,1,97')

# Inserting values
db.insertIntoTable("table1",valuesToisnert)

db.selectFromTable("table1")

keyboard_arrow_down Bad practice


It is possible to create a class without the __init__ method, but this is not a recommended
style because classes should describe homogeneous entities.
class Person:

def set_name(self, name):


self.name = name

def set_surname(self, surname):


self.surname = surname

def set_year_of_birth(self, year_of_birth):


self.year_of_birth = year_of_birth

def age(self, current_year):


return current_year - self.year_of_birth

def __str__(self):
return "%s %s was born in %d ." \
% (self.name, self.surname, self.year_of_birth)

In this case, an empty instance of the class Person is created, and no attributes have been
initialized while instantiating:

president = Person()

# This code will raise an attribute error:


print(president.name)

This raises an Attribute Error... We need to set the attributes:

president.set_name('John')
president.set_surname('Doe')
president.set_year_of_birth(1940)

print('Mr', president.name, president.surname,


'is the president, and he is very old. He is',
president.age(2014))

keyboard_arrow_down Protect your abstraction


Here the instance attributes shouldn't be accessible by the end user of an object as they are
powerful mean of abstraction they should not reveal the internal implementation detail. In
Python, there is no specific strict mechanism to protect object attributes but the official
guidelines suggest that a variable that has an underscore prefix should be treated as 'Private'.

Moreover prepending two underscores to a variable name makes the interpreter mangle a little
the variable name.
class Person:
def __init__(self, name, surname, year_of_birth):
self._name = name # _ single underscore means protected
self._surname = surname
self._year_of_birth = year_of_birth

def age(self, current_year):


return current_year - self._year_of_birth

def __str__(self):
return "%s %s and was born %d." \
% (self._name, self._surname, self._year_of_birth)

alec = Person("Alec", "Baldwin", 1958)


print(alec)
print(alec._surname)

class Person:
def __init__(a, name, surname, year_of_birth): # we can give whichever name we can gi
a.__name = name # __ double underscore means private members of a class
a.__surname = surname
a.__year_of_birth = year_of_birth

def age(self, current_year):


return current_year - self.__year_of_birth

def __str__(self):
return "%s %s and was born %d." \
% (self.__name, self.__surname, self.__year_of_birth)

alec = Person("Alec", "Baldwin", 1958)


print(alec._Person__name) # For accessing private elements of a class

__dict__ is a special attribute is a dictionary containing each attribute of an object. We can see
that prepending two underscores every key has _ClassName__ prepended.

keyboard_arrow_down 13.3 Inheritance


Once a class is defined it models a concept. It is useful to extend a class behavior to model a
less general concept. Say we need to model a Student, but we know that every student is also a
Person so we shouldn't model the Person again but inherit from it instead.

Through inheritance one can take all the methods and attributes from the another class and we
can override or extend the methods from the another class. The class which is overriding is
called child class and the class from methods are taking is called parent class.
class Student(Person):
def __init__(self, student_id, *args):
super(Student, self).__init__(*args)
self._student_id = student_id

charlie = Student(1, 'Charlie', 'Brown', 2006)


print(charlie._student_id)
print(type(charlie))
print(isinstance(charlie, Person))
print(isinstance(charlie, object))

Charlie now has the same behavior of a Person, but his state has also a student ID. A Person is
one of the base classes of Student and Student is one of the sub classes of Person. Be aware
that a subclass knows about its superclasses but the converse isn't true.

A sub class doesn't only inherits from its base classes, but from its base classes too, forming an
inheritance tree that starts from a object (every class base class).

super(Class, instance)

is a function that returns a proxy-object that delegates method calls to a parent or sibling class
of type. So we used it to access Person's __init__ .

# Another example
class StudentMarks(DataBaseOperations): # inheriting the DatabaseOperation class

def __init__ (self, ID, RollNumber, Marks):

self.id= ID
self.RollNum = RollNumber
self.Marks = Marks
self.databasename = "StudentDetails"

student1 = StudentMarks(23,34,76)

student1.createDatabase()

tableDetails = {"studentId" : "INTEGER", "studentRoll" : "INTEGER", "studentMarks" : "FLO

student1.createTable("studentMarks2",tableDetails)

valuestoInsert= ("{0},{1},{2}".format(student1.id,student1.RollNum,student1.Marks))

student1.insertIntoTable("studentMarks2",valuestoInsert)
student1.selectFromTable("studentMarks2")

keyboard_arrow_down Overriding methods


Inheritance allows to add new methods to a subclass but often is useful to change the behavior
of a method defined in the superclass. To override a method just define it again.

class Student(Person):
def __init__(self, student_id, *args, **kwargs):
super(Student, self).__init__(*args, **kwargs)
self._student_id = student_id

def __str__(self):
return super(Student, self).__str__() + " And has ID: %d" % self._student_id

charlie = Student(1, 'Charlie', 'Brown', 2006)


print(charlie)

We defined __str__ again overriding the one wrote in Person, but we wanted to extend it, so we
used super to achieve our goal.
#another class inheriting the DataBaseOperation class with overRiding the insert function
class StudentDetails(DataBaseOperations): # inheriting the DatabaseOperation class

def __init__ (self, FirstName,LastName, RollNumber, Class):

self.FirstName= FirstName
self.LastName = LastName
self.RollNumber = RollNumber
self.Class = Class
self.databasename = "StudentDetails"

#overriding the insert method of parent class to insert string values in table
def insertIntoTable(self,tablename):
try:

firstName = '"' + self.FirstName + '"' #putting string value under quotes


LastName = '"' + self.LastName + '"'
Class = '"' + self.Class + '"'

listOfvaluesToInsert= ("{0},{1},{2},{3}".format(firstName,LastName,self.R
conn = self.createDatabase()
conn.execute('INSERT INTO {tablename} values ({values})'.format(tablenam
conn.commit()
print("Values Inserted Successfully!!!")
self.closeDbConnection(conn)
print("Connection to database closed!!")
except Exception as e:
conn.rollback()
self.closeDbConnection(conn)
print("Connection to database closed!!")
print("Error occured: " + str(e))

student1 = StudentDetails("Raj","Kumar",34,"Ten")

student1.createDatabase()

tableDetails = {"studentFirstName" : "Varchar", "studentLastName" : "Varchar", "studentRo

student1.createTable("studentDetails",tableDetails)

student1.insertIntoTable("studentDetails")

student1.selectFromTable("studentDetails")

keyboard_arrow_down 13.4 Encapsulation


Encapsulation is an another powerful way to extend a class which consists on wrapping an
object with a second one.

Encapsulations is applications of a software or a program will not reveal the internal


implementation details to the outer world. The only constraint on the programmer is to maintain
the public interface for the component, as other programmers will be writing code that depends
on that developed interface. Encapsulation allows the implementation details of a program to be
change without affecting other parts of the program, thereby making it easier to fix bugs or add
new functionality with some local changes to an application.

There are two main reasons to use encapsulation:

Composition
Dynamic Extension

keyboard_arrow_down Composition
The abstraction process relies on creating a simplified model that remove useless details from
a concept. In order to be simplified, a model should be described in terms of other simpler
concepts. For example, we can say that a car is composed by:

Tyres
Engine
Body

And break down each one of these elements in simpler parts until we reach primitive data.
class Tyres:
def __init__(self, branch, belted_bias, opt_pressure):
self.branch = branch
self.belted_bias = belted_bias
self.opt_pressure = opt_pressure

def __str__(self):
return ("Tyres: \n \tBranch: " + self.branch +
"\n \tBelted-bias: " + str(self.belted_bias) +
"\n \tOptimal pressure: " + str(self.opt_pressure))

class Engine:
def __init__(self, fuel_type, noise_level):
self.fuel_type = fuel_type
self.noise_level = noise_level

def __str__(self):
return ("Engine: \n \tFuel type: " + self.fuel_type +
"\n \tNoise level:" + str(self.noise_level))

class Body:
def __init__(self, size):
self.size = size

def __str__(self):
return "Body:\n \tSize: " + self.size

class Car:
def __init__(self, tyres, engine, body):
self.tyres = tyres
self.engine = engine
self.body = body

def __str__(self):
return str(self.tyres) + "\n" + str(self.engine) + "\n" + str(self.body)

t = Tyres('Pirelli', True, 2.0)


e = Engine('Diesel', 3)
b = Body('Medium')
c = Car(t, e, b)
print(c)

Dynamic Extension

Sometimes it's necessary to model a concept that may be a subclass of another one, but it isn't
possible to know which class should be its superclass until runtime.

keyboard_arrow_down Example
Suppose we want to model a simple dog school that trains instructors too. It will be nice to re-
use Person and Student but students can be dogs or peoples. So we can remodel it this way:

class Dog:
def __init__(self, name, year_of_birth, breed):
self._name = name
self._year_of_birth = year_of_birth
self._breed = breed

def __str__(self):
return "%s is a %s born in %d." % (self._name, self._breed, self._year_of_birth)

kudrjavka = Dog("Kudrjavka", 1954, "Laika")


print(kudrjavka)

Start coding or generate with AI.

class Student:
def __init__(self, anagraphic, student_id):
self._anagraphic = anagraphic
self._student_id = student_id
def __str__(self):
return str(self._anagraphic) + " Student ID: %d" % self._student_id

alec_student = Student("dsfs",1)
kudrjavka_student = Student(kudrjavka, 2)

print(alec_student)
print(kudrjavka_student)
# Lets look at another example of encapsulation
class BonusDistribution:

def __init__ (self,employeeId, employeeRating):

self.empId = employeeId
self.empRating = employeeRating
self.__bonusforRatingA = "70%" #making value private
self.__bonusforRatingB = "60%" #making value private
self.__bonusforRatingC = "50%" #making value private
self.__bonusforRatingD = "30%" #making value private
self.__bonusforRatingForRest = "No Bonus" #making value private

def bonusCalculator(self):

if self.empRating == 'A':
bonus = self.__bonusforRatingA
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'B':
bonus = self.__bonusforRatingB
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'C':
bonus = self.__bonusforRatingC
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'D':
bonus = self.__bonusforRatingD
msg = "Bonus for this employee is :"+ bonus
return msg
else:
bonus = self.__bonusforRatingForRest
msg = "Bonus for this employee is :"+ bonus
return msg

emp1 = BonusDistribution(1232,'B')
emp2 = BonusDistribution(1342,'A')
emp3 = BonusDistribution(1031,'E')

emp2.bonusCalculator()

emp1.bonusCalculator()

emp3.bonusCalculator()

Let's try to change the private value of the class:

emp1._bonusforRatingB = "90%"
emp1.bonusCalculator()

The private attribute is not changed.

To change the private attribute we need to define a function inside the class. Let's see how.

class BonusDistribution:

def __init__ (self,employeeId, employeeRating):

self.empId = employeeId
self.empRating = employeeRating
self.__bonusforRatingA = "70%" #making value private
self.__bonusforRatingB = "60%" #making value private
self.__bonusforRatingC = "50%" #making value private
self.__bonusforRatingD = "30%" #making value private
self.__bonusforRatingForRest = "No Bonus" #making value private

def bonusCalculator(self):

if self.empRating == 'A':
bonus = self.__bonusforRatingA
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'B':
bonus = self.__bonusforRatingB
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'C':
bonus = self.__bonusforRatingC
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'D':
bonus = self.__bonusforRatingD
msg = "Bonus for this employee is :"+ bonus
return msg
else:
bonus = self.__bonusforRatingForRest
msg = "Bonus for this employee is :"+ bonus
return msg

def changeBonusForRatingForRest(self,value):

self.__bonusforRatingForRest = value

emp3 = BonusDistribution(1031,'E')

emp3.bonusCalculator()
emp3.changeBonusForRatingForRest("20%")

emp3.bonusCalculator()

We can see that the private attribute has now been changed and anyone can change that
attribute now. This is bad way of writing a method which can change an private attribute. Let's
make the function also private so it doesnot showup for everyone.

class BonusDistribution:

def __init__ (self,employeeId, employeeRating):

self.empId = employeeId
self.empRating = employeeRating
self.__bonusforRatingA = "70%" #making value private
self.__bonusforRatingB = "60%" #making value private
self.__bonusforRatingC = "50%" #making value private
self.__bonusforRatingD = "30%" #making value private
self.__bonusforRatingForRest = "No Bonus" #making value private

def bonusCalculator(self):

if self.empRating == 'A':
bonus = self.__bonusforRatingA
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'B':
bonus = self.__bonusforRatingB
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'C':
bonus = self.__bonusforRatingC
msg = "Bonus for this employee is :"+ bonus
return msg
elif self.empRating == 'D':
bonus = self.__bonusforRatingD
msg = "Bonus for this employee is :"+ bonus
return msg
else:
bonus = self.__bonusforRatingForRest
msg = "Bonus for this employee is :"+ bonus
return msg

def __changeBonusForRatingForRest(self,value):

self.__bonusforRatingForRest = value

emp3 = BonusDistribution(1031,'E')
emp3.bonusCalculator()

emp3

emp3.__changeBonusForRatingForRest("20%")

You can see that that method cannot be accessed now. Also, the method doesnot show up in
the class property:

If you know the name of the method, then you can still call the private member by using the
class name as shown below:

emp3._BonusDistribution__changeBonusForRatingForRest("20%")

emp3.bonusCalculator()

keyboard_arrow_down Operator Overloading


class multiplyNum():

def __init__(self,a):
self.a =a

a1 = multiplyNum(2)
a2 = multiplyNum(3)

#let's try and multiply both the objects


print(a1*a2)

We are getting an error because by default multiply supports only numerical values.

We can change the function of multiply and this is what we call overloading.

Python calls "mul" function to multiply numbers, let's overload it.

class multiplyNum():

def __init__(self,a):
self.a =a

def __mul__(self,other):

return self.a*other.a
a1 = multiplyNum(2)
a2 = multiplyNum(3)

a1*a2

Great!! now we can multiply objects. We can also overide our mul function and get it do return
sum instead of multiplication.

class multiplyNum():

def __init__(self,a):
self.a =a

def __mul__(self,other):

return self.a+other.a #overloading multiply method and returning sum instead of

a1 = multiplyNum(2)
a2 = multiplyNum(3)

a1*a2

keyboard_arrow_down Overload string method


class printInformation():

def __init__(self,operator):
self.operator =operator

def __str__(self):
return "overloading the opearator :" + self.operator

print_ = printInformation('string')

print(print_)

keyboard_arrow_down 13.5 Polymorphism and DuckTyping


Python uses dynamic typing which is also called as duck typing. If an object implements a
method you can use it, irrespective of the type. This is different from statically typed languages,
where the type of a construct need to be explicitly declared. Polymorphism is the ability to use
the same syntax for objects of different types:
def summer(a, b):
return a + b

print(summer(1, 1))
print(summer(["a", "b", "c"], ["d", "e"]))
print(summer("abra", "cadabra"))

# Polymorphism example
class Instagram:

def share_stories(self):
print("share your stories on Instagram!!!")

class Facebook:

def share_stories(self):
print("share your stories on Facebook!!!")

def ShareStory(application):
application.share_stories()

insta = Instagram()
fb = Facebook()

ShareStory(insta)

ShareStory(fb)

13.6 How long does a class should be?

There is an Object Oriented Programming (OOP) principle called Single Responsibility Principle
(SRP) and it states: "A class should have one single responsibility" or "A class should have only
one reason to change".

If you come across a class which doesn't follow the SRP principle, you should spilt it. You will be
grateful to SRP during your software maintenance.

There is an excellent example from wikipedia which is nicely understood the concept of SRP:-

"Martin defines a responsibility as a reason to change, and concludes that a class or module
should have one, and only one, reason to be changed (e.g. rewritten). As an example, consider a
module that compiles and prints a report. Imagine such a module can be changed for two
reasons. First, the content of the report could change. Second, the format of the report could
change. These two things change for very different causes; one substantive, and one cosmetic.
The single-responsibility principle says that these two aspects of the problem are really two
separate responsibilities, and should, therefore, be in separate classes or modules. It would be a
bad design to couple two things that change for different reasons at different times.

The reason it is important to keep a class focused on a single concern is that it makes the class
more robust. Continuing with the foregoing example, if there is a change to the report
compilation process, there is a greater danger that the printing code will break if it is part of the
same class." Source- Wikipedia

Refer:- https://en.wikipedia.org/wiki/Single-
responsibility_principle#:~:text=The%20single%2Dresponsibility%20principle%20(
SRP,the%20class%2C%20module%20or%20function.

keyboard_arrow_down 13.7 shallow and deep copy


Till now we have use = operator to create a copy of an object,like a=b. First we have thought that
it creates a new object,but rather it creates a new variable that take the reference of the same
original object.

#Lets look for the same = assignment operator reference id

a=10
b = a

print("Id of a:- " , id(a))


print("Id of b:- ", id(b))

# Here the id is same for both a & b, hence if we want to change the value it will reflec

a = 20

print(a,b)

But many times we want to change the newly created values only, we dont want to modify the
original value so for that we can achieve this with creating copies in two ways:-

1) Deep Copy

2) Shallow Copy

keyboard_arrow_down Shallow copy


A shallow copy creates a new object which stores the reference of the original elements.
So, a shallow copy doesn't copy actual object, instead it just copies the reference of nested
objects. Lets look with example:-

a = ["ineuron","datascience"]
# Shallow copy can be created using copy module
import copy
a_new = copy.copy(a)

print(a)
print(a_new)

print(id(a))
print(id(a_new)) # Id will be different

# Lets reassign the values


a.append("AI")

print(a)
print(a_new)

# Try to update the original nested value


import copy

a = [[1, 2, 3], [4, 5, 6],[5,5,5]]


new_a = copy.copy(a)

a[1][1] = 'Update_value'

print(a)
print(new_a)

keyboard_arrow_down Deep copy


The deep copy will create independent copy of original object rather than just copying reference
and all the other nested objects as well.

We can create deep copy using deepcopy() function present in copy module.

import copy

a = [1,2,3]
a_new = copy.deepcopy(a)

print(a)
print(a_new)

print(id(a))
print(id(a_new))
a[1]="change"
print(a)
print(a_new)

a = [[1,2,3],[4,5,6],[7,8,9]]

a_new = copy.deepcopy(a)

print(a)
print(a_new)

a[0][0] = "update"

print(a)
print(a_new) # Not changed this value
# Few more oops example

# Single inheritence
class Ineuron:
company_website = ""
name = ''

def contact_details(self):
print('Contact us at ', self.company_website)

class Datascience(Ineuron):
def __init__(self):
self.year_of_establishment= 2018

def est_details(self):
print('{0} Company was established in {1}'
.format(self.name,self.year_of_establishment))

ds = Datascience()
ds.est_details()

# Multiple inheritence
class OS:
multi_task = True
os_name = 'Windows OS'

class windows(OS, comp):


def __init__(self):
if self.multi_task is True:
print('multi_task')
print('Name: {}'.format(self.name))

windows = windows()

# Multilevel inheritence
class champ:
num_of_courses = 12

class Datascience(champ):
course_type = 'Data-Science'

class AI(Datascience):
def __init__(self):
self.company = "xyz"
print('The company {0} offers total {1} different types of courses. Most trending
AI = AI()

OOP is a programming paradigm (way of developing programs)

OOP allows us to combine the data and functionality and wrap it inside something which is

In programming any real world entity which has specific attributes or features can be
represented as an Object.

Along with attributes each object can take some actions also which are called it’s “behaviors”

In programming world, these attributes are called data members and behaviours/actions are
called methods

Yes , we humans are objects because:

We have attributes as name, height, age etc.

We also can show behaviors like walking, talking, running, eating etc

Now to create/represent objects we first have to write all their attributes and behavio

This group is called a class

keyboard_arrow_down Class
A class is used to specify the basic structure of an object and it combines attributes an

Thus we can say that a class represents the data type and object represents a kind of var

For Example:- Each person collectively come under a class called Human Being. So we belo

image.png
image.png

class Emp:
pass

e=Emp()

print(type(e))

print(e)

id(e)

1.The first line shows the class name which is Emp.

2. The second line shows the address of the object to which the reference e is pointing

3. The name __main__ is the name of the module which Python automatically allots to our f

In Python , a class can have 3 types of variables:

Instance Variables: Created per instance basis

Local Variables: Created locally inside a method and destroyed when the method execution

Class Variables: Created inside a class and shared by every object of that class. Sometim

class Emp:
def __init__(self):
print("Object created. . .")
print(id(self))

e=Emp()
print(id(e))

In Python , whenever we create an object , Python calls the method __init()__

But while calling this method , Python also passes the address of the object , for which

Thus , when we define the __init__() method we must provide it atleast one formal argumen

This argument is named as self


class Emp:
def __init__(self):
print(id(self))
self.age=25
self.name="Rahul"
self.salary=30000.0

e=Emp()
print(id(e))
print("Age:",e.age,"Name:",e.name,"Salary:",e.salary)

class Emp:
def __init__(self):
self.age=25
self.name="Rahul"
self.salary=30000
def show(self):
print(self.age,self.name,self.salary)

e=Emp()

f=Emp()

e.show()

f.show()

print("Age:",e.age,"Name:",e.name)

print("Age:",self.age,"Name:",self.name)

e.salary=30000.0

print("Age:",e.age,"Name:",e.name,e.salary)

class Emp:
def __init__(self,age,name,salary):
self.age=age
self.name=name
self.salary=salary
e=Emp(25,"Rahul",30000.0)

print("Age:",e.age,"Name:",e.name,"Salary:",e.salary)

f=Emp(31,"Varun",45000.0)

print("Age:",f.age,"Name:",f.name,"Salary:",f.salary)

class Emp:
def __init__(self,x,y,z):
self.age=x
self.name=y
self.salary=z

e=Emp(25,"Rahul",30000.0)
print("Age:",e.age,"Name:",e.name,"Salary:",e.salary)
f=Emp(31,"Varun",45000.0)
print("Age:",f.age,"Name:",f.name,"Salary:",f.salary)

class Emp:
def __init__(self,name):
self.name=name
def __init__(self,name,age):
self.name=name
self.age=age
def __init__(self,name,age,sal):
self.name=name
self.age=age
self.sal=sal

e1=Emp("amit")

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-980ca38a04a0> in <module>
----> 1 e1=Emp("amit")

TypeError: __init__() missing 2 required positional arguments: 'age' and 'sal'

e2=Emp("sumit",23)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-6-f7e73b0e25c4> in <module>
----> 1 e2=Emp("sumit",23)

TypeError: __init__() missing 1 required positional argument: 'sal'

e3=Emp("deepak",34,50000.)

print(e1.name)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-8-0f3f6405404d> in <module>
----> 1 print(e1.name)

NameError: name 'e1' is not defined

print(e2.name,e2.age)

---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-9-7e662a0a512c> in <module>
----> 1 print(e2.name,e2.age)

NameError: name 'e2' is not defined

print(e3.name,e3.age,e3.sal)

deepak 34 50000.0

class Emp:
def __init__(self,name,age=0,sal=0.0):
self.name=name
self.age=age
self.sal=sal

e1=Emp("amit")
e2=Emp("sumit",23)
e3=Emp("deepak",34,50000.)
print(e1.name)
print(e2.name,e2.age)
print(e3.name,e3.age,e3.sal)
class Emp:
def __init__(self,*name):
self.name=name
#self.age=age
#self.sal=sal

#e1=Emp("amit")
#e2=Emp("sumit",23)
e3=Emp("deepak",34,50000.)

print(e3.name)
#print(e2.name,e2.age)
#print(e3.name,e3.age,e3.sal)

('deepak', 34, 50000.0)

Types Of Methods

Adding Instance Methods

Obtaining Details Of Instance Variables

Different Ways To Create Instance Variables

Deleting Instance Variables

In Python , a class can have 3 types of methods:

Instance Methods: Called using object

Class Methods: Called using class name

Static Methods: Called using class name

Instance methods are the most common type of methods in Python classes.

These are called instance methods because they can access instance members of the object

These methods always take atleast one parameter, which is normally called self, which po

Through the self parameter, instance methods can access data members and other methods on

This gives them a lot of power when it comes to modifying an object’s state.
class Emp:
def __init__(self,age,name,salary):
print(id(self))
self.age=age
self.name=name
self.salary=salary
def show(self):
print("Age:",self.age,"Name:",self.name,"Salary:",self.salary)

e=Emp(25,"Rahul",30000.0)

1471557586656

print(id(e))

1471557586656

f=Emp(31,"Varun",45000.0)

1471557586464

print(id(f))

1471557586464

e.show()
f.show()

Age: 25 Name: Rahul Salary: 30000.0


Age: 31 Name: Varun Salary: 45000.0

class Emp:
def __init__(self,name,age,sal):
name=name
age=age
sal=sal
print(name,age,sal)
def show(self):
print(self.age,self.name,self.sal)

e1=Emp("amit",34,50000.0)

amit 34 50000.0

e1.show()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-28-322691ee7613> in <module>
----> 1 e1.show()

<ipython-input-26-c596fa30346d> in show(self)
5 sal=sal
6 def show(self):
----> 7 print(self.age,self.name,self.sal)

AttributeError: 'Emp' object has no attribute 'age'

Every object in Python has an attribute denoted by dict.

This attribute is automatically added by Python and it contains all the attributes defined for the
object itself.

It maps the attribute name to its value.

class Emp:

def __init__(self):
self.name="Amit"
self.age=24
self.sal=50000.0

e1=Emp()

print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0}

class Emp:
def __init__(self):
self.name="Amit"
self.age=24
def set_sal(self):
self.sal=50000.0

e1=Emp()
print(e1.__dict__)

{'name': 'Amit', 'age': 24}

e1.set_sal()

print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0}

class Emp:
def __init__(self):
self.name="Amit"
self.age=24
self.sal=50000.0
def show(self):
print(self.name,self.age,self.sal,self.department)

e1=Emp()

print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0}

e1.__dict__['department']='IT'

print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0, 'department': 'IT'}

e1.show()

Amit 24 50000.0 IT

Since dict is a dictionary , we can manipulate it and add/del instance members from it

class Emp:
def __init__(self):
self.name="Amit"
self.age=24
self.sal=50000.0
def show(self):
print(self.name,self.age,self.sal,self.department)

e1=Emp()
print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0}

e1.__dict__['department']='IT'

print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0, 'department': 'IT'}

e1.show()

Amit 24 50000.0 IT

del e1.__dict__['age']

e1.show()

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-56-322691ee7613> in <module>
----> 1 e1.show()

<ipython-input-49-e85b7c51a657> in show(self)
5 self.sal=50000.0
6 def show(self):
----> 7 print(self.name,self.age,self.sal,self.department)

AttributeError: 'Emp' object has no attribute 'age'

print(e1.__dict__)

{'name': 'Amit', 'sal': 50000.0, 'department': 'IT'}

dic={}

dic2={'1':"sunny","2":"savita"}

dic.update(dic2)

dic

{'1': 'sunny', '2': 'savita'}


class Emp:
def __init__(self,**kwargs):
self.__dict__.update(kwargs)
def show(self):
print(self.name,self.age,self.sal)

e1=Emp(name="Amit",age=24,sal=50000.0)

e1.show()

Amit 24 50000.0

Till now we can say there are 4 ways in Python to create instance variables:

Inside the constructor/__init__() method using self

Inside any instance method of the class using self

Outside the class using it’s object reference

Using the instance attribute __dict__

class Emp:

def __init__(self,name,age,sal):
self.name=name
self.age=age
self.sal=sal
def setDept(self,department):
self.department=department
def setProject(self,project):
self.project=project
def setBonus(self,bonus):
self.bonus=bonus
def remove(self):
del self.name
del self.age

e1=Emp("Amit",24,30000.0)

e1.setDept("Finance")
e1.setProject("Banking Info System")
e1.setBonus(20000.0)
print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 30000.0, 'department': 'Finance', 'project': 'Ban

e1.remove()

e1.__dict__

{'sal': 30000.0,
'department': 'Finance',
'project': 'Banking Info System',
'bonus': 20000.0}

del e1.sal

e1.__dict__

{'department': 'Finance', 'project': 'Banking Info System', 'bonus': 20000.0}

e2=Emp("Sumit",34,45000.0)

e2.setDept("Production")

print()
print(e2.__dict__)

We can delete/remove instance variables in 2 ways:

Using del self .<var_name> from the body of any instance method within the class

Using del <obj_ref>.<var_name> from outside the class

Adding Class Variables

Different Ways To Create A Class Variable

Different Ways To Access A Class Variable

Obtaining Details Of Class Variables

Deleting Class Variables


Class variables are those variables which are defined within the class body outside any m

They are also called as static variables , although there is no static keyword used with

The are shared by all instances of the class and have the same value for each instance of

They have a single copy maintained at the class level

The term class level means inside the class object.

In Python , for every class one special object is created called as class object

Don’t think it is the same object which we create. No it is not that!

Rather , for every class , Python itself creates an object called as class object and ins

We can use a class variable at 6 places in Python:

Inside the class body but outside any method

Inside the constructor using the name of the class

Inside instance method using name of the class

Inside classmethod using name of the class or using the special reference cls

Inside staticmethod using the name of the class

From outside the class using name of the class

We must clearly understand the difference between accessing and modifying .

Accessing means we are just reading the value of the variable

Modifying means we are changing it’s value

The class variables can be accessed in 4 ways:

Using name of the class anywhere in the program

Using self inside any instance method

Using object reference outside the class

Using special reference cls inside classmethod


The class variables can be modified in 3 ways:

Using name of the class anywhere inside the methods of the class

Using special reference cls inside classmethod

Using name of the class outside the class body

Special Note:We must never modify a class variable using self or object reference , becau

class CompStudent:
stream = 'cse'
def __init__(self,name,roll):
self.name = name
self.roll = roll

obj1 = CompStudent('Atul',1)

print(obj1.name)
print(obj1.roll)
print(obj1.stream)

Atul
1
cse

CompStudent.stream

'cse'

obj2 = CompStudent('Chetan', 2)

print(obj2.name)
print(obj2.roll)
print(obj2.stream)

Chetan
2
cse

print(CompStudent.stream)

cse
class CompStudent:

def __init__(self,name,roll):
CompStudent.stream='cse'
self.name = name
self.roll = roll
def test(self):
CompStudent.schoolname="adsnak"

obj1= CompStudent('Chetan', 2)

print(obj1.name)
print(obj1.roll)
print(obj1.stream)

Chetan
2
cse

obj1.test()

print(obj1.schoolname)

adsnak

print(obj2.name)
print(obj2.roll)
print(obj2.stream)
print(CompStudent.stream)

As we know , class variables are owned by a class itself (i.e., by its definition), so to

Thus we can see that Python has 2 dictionaries called __dict__ .

One is <class_name>.__dict__ and the other is <object_ref>.__dict__


class Emp:
raise_per=7.5
comp_name="Google"
def __init__(self):
self.name="Amit"
self.age=24
self.sal=50000.0

e1=Emp()

print(e1.__dict__)

{'name': 'Amit', 'age': 24, 'sal': 50000.0}

print(Emp.__dict__)

{'__module__': '__main__', 'raise_per': 7.5, 'comp_name': 'Google', '__init__': <fun

class Sample:
i=10
def __init__(self):
Sample.j=20
def f1(self):
Sample.k=30
Sample.m=40
print(Sample.__dict__)

Why the code is showing only 2 class variables even though we have 4 ?

This is because the class variable k will only be created when f1() gets called . Similar

class Sample:
i=10
def __init__(self):
Sample.j=20
def f1(self):
Sample.k=30
Sample.m=40
s1=Sample()
print(Sample.__dict__)
class Sample:
i=10
def __init__(self):
Sample.j=20
def f1(self):
Sample.k=30
Sample.m=40
s1=Sample()
s2=Sample()
s1.f1()
s2.f1()
print(Sample.__dict__)

class Sample: i=10 def init(self): print("Constructor called. . .") print(Sample.i) print(self.i) def
f1(self): print("f1 called. . .") print(Sample.i) print(self.i)

s1=Sample() s1.f1() print(Sample.i) print(s1.i)

Class Methods

Creating Class Methods

Accessing Class Methods

Static Methods

Accessing Static Methods

Difference Between Instance Method , Class Method and Static Methods

Just like we can have class variables , similarly Python also allows us to create class methods.

These are those methods which work on the class as a whole , instead of working on it’s object.

For , example in our Emp class if we want to initialize the class variable raise_per inside a
method , then the best way would be to create a class method for this purpose

To create a class method we write the special word @classmethod on top of method definiti

Syntax:
class <class_name>:
@classmethod #decorator
def <method_name>(cls)
// class specific code

Notice that a class method gets a special object reference passed as argument by Python
called as class refercnce
To define a class method it is compulsory to use the decorator @classmethod

ClassMethods can only access class level data and not instance specific data

Just like Python passed self as argument to instance methods , it automatically passes cls as
argument to classmethods

The argument cls is always passed as the first argument and represents the class object.

Recall , that for every class Python creates a special object called class object , so the reference
cls points to this object.

The name cls is just a convention , although we can give any name to it.

To call a classmethod we simply prefix it with classname followed by dot operator.

Although we can use object reference also to call a classmethod but it is highly recommended
not to do so , since classmethods do not work upon individual instances of the class

Write a program to create a class called Emp , having an instance members called name , age
and sal . Also declare a class variable called raise_amount to store the increment percentage of
sal and set it the value given by the user Now provide following methods in your class

init_() : This method should initialize instance members with the parameter passed
increase_sal(): This method should calculate the increment in sal and add ot to the instance
member sal display(): This method should display name , age and sal of the employee Finally , in
the main script , create 2 Emp objects , initialize them and increase their salary . Finally display
the data

class Emp:
raise_amount=0
@classmethod
def set_raise_amount(cls):
print(id(cls))
cls.raise_amount=float(input("Enter raise percentage:"))##This can can also be wr

def __init__(self,name,age,sal):
print(id(self))
self.name=name
self.age=age
self.sal=sal
def increase_sal(self):
self.sal=self.sal+(self.sal*Emp.raise_amount/100)
def display(self):
print(self.name,self.age,self.sal)
Emp.set_raise_amount()

1759103895504
Enter raise percentage:10

e1=Emp("Amit",24,50000.0)
print(id(e1))

1759130971008
1759130971008

e1.display()

Amit 24 50000.0

e1.increase_sal()

e1.display()

Amit 24 55000.0

class Emp:
def __init__(self,name,age,sal):
print(id(self))
self.name=name
self.age=age
self.sal=sal
def increase_sal(self):
self.sal=self.sal+(self.sal*Emp.raise_amount/100)
def display(self):
print(self.name,self.age,self.sal)

Emp.set_raise_amount()

e1=Emp("Amit",24,50000.0)

1759114747040

e1.display()

Amit 24 50000.0

print(id(e1))

1759114747040

e2=Emp("Sunny",25,25000.0)
e2.display()

Sunny 25 25000.0

e2.increase_sal()

e2.display()

Sunny 25 27500.0

Emp.set_raise_amount()

Enter raise percentage:50

e2.increase_sal()

e2.display()

Sunny 25 41250.0

print("Before incrementing :")


print("_____________________");
e1.display()
e2.display()
e1.increase_sal()
e2.increase_sal()
print()
print("After incrementing by",Emp.raise_amount,"percent:")
print("__________________________________");
e1.display()
e2.display()

Another very important use of classmethod is to work as Factory methods

Factory methods are those methods which return a class object (like constructor) for diff

It is similar to function overloading in C++ , but since, Python doesn't have anything as
To understand this , suppose we want to create a class called Date , which allows us to i

Obviously , we will have __init__() method , which will accept 3 parameters representing

class Date:
def __init__(self, day=10, month=10, year=2000):
self.day=day
self.month=month
self.year=year
def show(self):
print(self.day,self.month,self.year,sep="/")

d1=Date(10,12,2016)
d1.show()

Now suppose we also want to allow date arguments to be passed as a single string (“10-12

For this we can create a classmethod in the class which accepts a string representing dat

class Date:
def __init__(self, day=10, month=10, year=2000):
self.day=day
self.month=month
self.year=year
def show(self):
print(self.day,self.month,self.year,sep="/")
@classmethod
def from_string(cls, string_date):
day, month, year = map(int, string_date.split('-'))
myDate = cls(day, month, year)
return myDate

d1=Date(10,12,2016)
d1.show()
d2=Date.from_string("15-11-2017")
d2.show()
keyboard_arrow_down Static method
The third type of method a Python class can contain are called static methods

Static methods, much like class methods, are methods that are bound to a class rather than it’s
object.

Just like class methods , they also do not require any object to be called and can be called using
name of the class

The difference between a static method and a class method is: Static method knows nothing
about the class and just deals with the parameters.

Class method works with the class since it’s parameter is always the class itself.

This means that a static method doesn’t even get cls reference unlike a class method

It knows nothing about the class and is only interested to work upon it’s parameters

To create a static method we write the decorator @staticmethod on top of method definitio

Syntax:
class <class_name>:
@staticmethod
def <method_name>(<arg_list> )
pass ##argument specific code

#Notice that a static method doesn’t get any implicit argument by Python

class MyMath:
@staticmethod
def add_nos(a,b):
c=a+b
return c
@staticmethod
def mult_nos(a,b):

You might also like