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

PP_Module-4_Notes

Uploaded by

bhuvanvasa23s
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views

PP_Module-4_Notes

Uploaded by

bhuvanvasa23s
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 41

MODULE-4

Classes and Object-Oriented Programming (OOP)

OBJECT-ORIENTED PROGRAMMING (OOP)

INTRODUCTION
 Object-oriented programming is a programming paradigm that provides a means of
structuring programs so that properties and behaviors are bundled into individual objects.
 For instance, an object could represent a person with properties like a name, age, and
address and behaviors such as walking, talking, breathing, and running. Or it could
represent an email with properties like a recipient list, subject, and body and behaviors
like adding attachments and sending.
 Put another way, object-oriented programming is an approach for modeling concrete,
real-world things, like cars, as well as relations between things, like companies and
employees, students and teachers, and so on.
 OOP models real-world entities as software objects that have some data associated with
them and can perform certain functions.
 As Python is a multi-paradigm programming language, it will supports different
programming approaches.
 So it uses Object-Oriented Programming (OOP) approach to solve a programming
problem by creating objects.

Procedural Oriented Programming Object Oriented Programming

In procedural programming, program In object oriented programming,


is divided into small parts program is divided into small parts
called functions. called objects.

It does not support inheritance. It supports inheritance.

Object oriented programming have


There is no access specifiers in
access specifiers like private, public,
procedural programming.
protected etc.

Adding new data and function is not


Adding new data and function is easy.
easy.
Procedural programming does not
Object oriented programming provides
have any proper way for hiding data
data hiding so it is more secure.
so it is less secure.

In procedural programming, Overloading is possible in object


overloading is not possible. oriented programming.

In procedural programming, function In object oriented programming, data is


is more important than data. more important than function.

Procedural programming is based Object oriented programming is based


on unreal world. on real world.

Examples: C, FORTRAN, Pascal,


Examples: C++, Java, Python, C# etc.
Basic etc.

Major principles of object-oriented programming system are given below:


 Class
 Object
 Method
 Inheritance
 Polymorphism
 Data Abstraction
 Encapsulation

Class:
 A class is a blueprint for the object.
 The class can be defined as a collection of objects.
 It is a logical entity that has some specific attributes and methods.
 For example: if you have an employee class, then it should contain an attribute and
method, i.e. an email id, name, age, salary, etc.
 Class is defined under a “Class” Keyword.
 Example (Empty class):
class student:
pass

Object:
 An object (instance) is an instantiation of a class.
 When class is defined, only the description for the object is defined. Therefore, no
memory or storage is allocated.
 The example for object of student class can be:
s1 = student() Here, s1 is an object of class student.
 Similarly we can create multiple objects for single class.

Method:
 The method is a function that is associated with an object. In Python, a method is not
unique to class instances. Any object type can have methods.

Inheritance:
 Inheritance is the most important aspect of object-oriented programming, which
simulates the real-world concept of inheritance. It specifies that the child object acquires
all the properties and behaviors of the parent object.
 By using inheritance, we can create a class which uses all the properties and behavior of
another class. The new class is known as a derived class or child class, and the one whose
properties are acquired is known as a base class or parent class.It provides the re-usability
of the code.

Polymorphism:
 Polymorphism contains two words "poly" and "morphs". Poly means many, and morph
means shape(forms).
 It is an ability (in OOP) to use a common interface for multiple forms (data types).
 By polymorphism, we understand that one task can be performed in different ways.
 Suppose, we need to color a shape, there are multiple shape options (rectangle, square,
circle). However we could use the same method to color any shape. This concept is called
Polymorphism.
 Another example - you have a class animal, and all animals speak. But they speak
differently. Here, the "speak" behavior is polymorphic in a sense and depends on the
animal.

Encapsulation:
 Encapsulation is also an essential aspect of object-oriented programming.
 It is used to restrict access to methods and variables.
 This prevents data from direct modification which is called encapsulation.
 In encapsulation, code and data are wrapped together within a single unit from being
modified by accident.

Abstraction:
 Abstraction is used to hide the internal functionality of the function from the users.
 The users only interact with the basic implementation of the function, but inner working
is hidden. User is familiar with that "what function does" but they don't know "how it
does."
 In simple words, we all use the smart phone and very much familiar with its functions
such as camera, voice-recorder, call-dialing, etc., but we don't know how these operations
are happening in the background.
 Let's take another example - When we use the TV remote to increase the volume. We
don't know how pressing a key increases the volume of the TV. We only know to press
the "+" button to increase the volume.
 That is exactly the abstraction that works in the object-oriented concept.

CLASS
 A Python class is a group of attributes and methods.

What is Attribute?
Attributes are represented by variable that contains data.

What is Method?
Method performs an action or task. It is similar to function.

How to Create Class:


class Classname(object) :
def __init__(self):
self.variable_name = value
self.variable_name = value
def method_name(self):
Body of Method

 class - class keyword is used to create a class


 object - object represents the base class name from where all classes in Python are
derived. This class is also derived from object class. This is optional.
 __init__() – This method is used to initialize the variables. This is a special method. We
do not call this method explicitly.
 self – self is a variable which refers to current class instance/object.

Rules:
 The class name can be any valid identifier.
 It can't be Python reserved word.
 A valid class name starts with a letter, followed by any number of letter, numbers or
underscores.
 A class name generally starts with Capital Letter.

Example:

1. class Mobile:
def __init__(self):
self.model = ‘RealMe X’

def show_model (self):


print(‘Model:’, self.model)
2. class Mobile:
def __init__(self, m):
self.model = m
def show_model (self, p):
price = p # Local Variable
print(‘Model:’, self.model, ‘Price:’, price)

OBJECT (INSTANCE)
 Object is class type variable or class instance. To use a class, we should create an object
to the class.
 Instance creation represents allotting memory necessary to store the actual data of the
variables.
 Each time you create an object of a class a copy of each variables defined in the class is
created.
 In other words you can say that each object of a class has its own copy of data members
defined in the class.
Syntax: -
object_name = class_name()
object_name = class_name(arg)

Example:

1. class Mobile:
def __init__(self):
self.model = ‘RealMe X’
def show_model (self):
print(‘Model:’, self.model)
realme = Mobile()

2. class Mobile:
def __init__(self, model):
self.model = model
def show_model (self):
print(‘Model:’, self.model)

realme = Mobile(‘RealMe X’)

realme = Mobile()
 A block of memory is allocated on heap. The size of allocated memory is to be decided
from the attributes and methods available in the class (Mobile).
 After allocating memory block, the special method __init__() is called internally. This
method stores the initial data into the variables.
 The allocated memory location address of the instance is returned into object (realme).
 The memory location is passed to self.

Accessing class member using object(instance):

 We can access variable and method of a class using class object or instance of class.

object_name.variable_name
realme.model

object_name.method_name ( )
realme.show_model ( );

object_name.method_name (parameter_list)
realme.show_model(1000);

self Variable:
 self is a default variable that contains the memory address of the current object.
 This variable is used to refer all the instance variable and method.
 When we create object of a class, the object name contains the memory location of the
object.
 This memory location is internally passed to self, as self knows the memory address of
the object so we can access variable and method of object.
 self is the first argument to any object method because the first argument is always the
object reference. This is automatic, whether you call it self or not.
def __init__(self):
def show_model(self):

Object (Instance) Example:

 Each time you create an object of a class a copy of each variables defined in the class
is created.
class Mobile:
def __init__(self,model):
self.model = Model
def show_model (self):
print(‘Model:’, self.model)
realme = Mobile(“Realme X”)
redmi = Mobile(“Redmi 9”)
poco = Mobile(“Poco M2”)
Constructor:
 Python supports a special type of method called constructor for initializing the instance
variable of a class.
 A class constructor, if defined is called whenever a program creates an object of that
class.
 A constructor is called only once at the time of creating an instance.
 If two instances are created for a class, the constructor will be called once for each
instance.

Constructor without Parameter:

class Mobile:
def __init__(self):
self.model =‘RealMe X’
realme = Mobile( )

Constructor with Parameter:

1. class Mobile:
def __init__(self, m):
self.model = m
realme = Mobile('Realme X')

2. class Mobile:
def __init__(self, m, v):
self.model = m
self.volumn = v
redmi = Mobile('Redmi 7s', 50)

Deleting Attributes of an Object:


 Python gives us two built-in ways to delete attributes from objects, the del command
word and the delattr built-in function.

Deleting attribute using del statement:


Syntax:

del object_name.attribute_name

Deleting attribute using del statement:


class Mobile:
def __init__(self,model,price):
self.model = model
self.price = price
realme = Mobile("Realme X","25000")
print(realme.model,realme.price)
#deleting price attribute of realme object using del statement
del realme.price
print(realme.model)
print(realme.price)
#for the last line it results an attribute error as price is no longer an attribute

Deleting attribute using delattr() built in function:


 The delattr() method is used to delete the named attribute from the object, with the prior
permission of the object.
Syntax:
delattr(object, name)
 The function takes only two parameter:
object :from which the name attribute is to be removed.
name :of the attribute which is to be removed.
 The function doesn't returns any value, it just removes the attribute, only if the object
allows it.

class Mobile:
def __init__(self,model,price):
self.model = model
self.price = price
realme = Mobile("Realme X","25000")
print(realme.model,realme.price)

#deleting price attribute of realme object using delattr()


delattr(realme,"price")
print(realme.model)
print(realme.price)

#for the last line it results an attribute error as price is no longer an attribute

Deleting Objects:
 We can delete the object itself using the del statement.
class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
e1=employee("XYZ","30000")
#It prints XYZ
print(e1.name)
#Deleting e1 object
del e1
#It results in name error,e1 is not defined
print(e1.name)

 When we create an e1 object , a new instance object is created in memory and the name
e1 binds with it.
 When we delete the object using the del e1 statement, this binding is removed and the
name e1 is deleted from the corresponding namespace.
 The object, however, continues to exist in memory and if no other name is bound to it , it
is later automatically destroyed.
 This automatic destruction of unreferenced objects in Python is also called garbage
collection.

Destructors in Python:
 Just like a constructor is used to create and initialize an object, a destructor is used to
destroy the object and perform the final clean up.
 Although in python we do have garbage collector to clean up the memory, but its not
just memory which has to be freed when an object is dereference or destroyed, it can be a
lot of other resources as well, like closing open files, closing database connections,
cleaning up the buffer or cache etc.
 Hence when we say the final clean up, it doesn't only mean cleaning up the memory
resources.
 The __del__() method is used as the destructor method in Python. The user can call
the __del__() method when all the references of the object have been deleted, and it
becomes garbage collected.

class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __del__(self):
print("Destructor called")

e1=employee("XYZ","30000")
print(e1.name)
print(e1.salary)
del e1
Output:
XYZ
30000
Destructor called

 Note: As there is only one reference e1 for object, so upon deleting reference e1,
__del__( ) method is called . Because for calling destructor reference count should be
zero.

class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __del__(self):
print("Destructor called")
e1=employee("XYZ","30000")
e2=e1
e3=e1
print(e1.name)
print(id(e1),id(e2),id(e3))
del e1
Output:
XYZ
57824984 57824984 57824984

Note: As there are multiple references such as e1,e2,e3 for object, so upon deleting reference e1,
__del__( ) method will not be called . Because for calling destructor reference count should be
zero but here still the reference count is 2.

class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __del__(self):
print("Destructor called")
e1=employee("XYZ","30000")
e2=e1
e3=e1
print(e1.name)
del e1
print(e2.salary)
del e2
print(e3.name)
del e3
Output:
XYZ
30000
XYZ
Destructor called

Note:1.Finally __del__() method is called as reference count became zero.


2. Invoking del e1 does not call e1.__del__() as you saw above, it just decrements the reference
count of e1.
3. The destructor is called exactly once, the first time the reference count goes to zero and the
object deallocated.

CLASS ATTRIBUTES
 As an object-oriented language, Python provides two scopes for attributes: class attributes
and instance attributes.
 Class attributes are tied only to the classes in which they are defined, and since instance
objects are the most commonly used objects in everyday OOP, instance data attributes are
the primary data attributes you will be using.
 Class data attributes are useful only when a more "static" data type is required which is
independent of any instances.
 So a class attribute is a Python variable that belongs to a class rather than a particular
object.
 It is shared between all the objects of this class and it is defined outside the constructor
function, __init__(self,...), of the class.

Class Data Attributes:


 Data attributes are simply variables of the class we are defining. They can be used like
any other variable in that they are set when the class is created and can be updated either
by methods within the class or elsewhere in the main part of the program.
 Such attributes are better known to OO programmers as static members, class variables,
or static data.
 They represent data that is tied to the class object they belong to and are independent of
any class instances.
 If you are a Java or C++ programmer, this type of data is the same as placing the static
keyword in front of a variable declaration.

Example:
class Employee:
Company="TCS" # Class Attribute
print(Employee.Company)
#Output is: TCS

Determining Class Attributes:


 There are two ways to determine what attributes a class has. The simplest way is to use
the dir() builtin function.
 An alternative is to access the class dictionary attribute __dict__, one of a number of
special attributes that is common to all classes.

Let us take a look at an example:


 >>>dir(Employee)
 ['Company', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__']

 >>>Employee.__dict__
 mappingproxy({'__module__': '__main__', '__doc__': 'Employee is a class with Company
as class attribute', 'Company': 'TCS', '__dict__': <attribute '__dict__' of 'Employee'
objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>})

 As dir() returns a list of (just the) names of an object's attributes while __dict__ is a
dictionary whose attribute names are the keys and whose values are the data values of the
corresponding attribute objects.
 The output also reveals __doc__ and __module__, which are special class attributes that
all classes have (in addition to __dict__).

Special Class Attributes


 For any class C, Below it represents a list of all the special attributes of C:

Special Class Attributes


 C.__name__ String name of class C
 C.__doc__ Documentation string for class C
 C.__bases__ Tuple of class C's parent classes
 C.__dict__ Attributes of C
 C.__module__ Module where C is defined
 C.__class__ Class of which C is an instance

>>>Employee.__name__
'Employee'
>>>Employee.__doc__
'Emplyee is a class with Company as class attribute'
>>>Employee.__bases__
(<class 'object'>,)
>>>Employee.__dict__
mappingproxy({'__module__': '__main__', '__doc__': 'Emplyee is a class with Company as class
attribute', 'Company': 'TCS', '__dict__': <attribute '__dict__' of 'Employee' objects>,
'__weakref__': <attribute '__weakref__' of 'Employee' objects>})
>>>Employee.__class__
<class 'type'>
>>>Employee.__module__
'__main__'

 __name__ is the string name for a given class. This may come in handy in cases where a
string is desired rather than a class object. Even some built-in types have this attribute,
and we will use one of them to showcase the usefulness of the __name__ string.
 __doc__ is the documentation string for the class, similar to the documentation string for
functions and modules, and must be the first unassigned string succeeding the header
line.
 __bases__ deals with inheritance, which we will cover later in this chapter; it contains a
tuple that consists of a class's parent classes.
 The aforementioned __dict__ attribute consists of a dictionary containing the data
attributes of a class.
 Python supports class inheritance across modules. To better clarify a class's description,
the __module__ was introduced so that a class name is fully qualified with its module.
 When we access the __class__ attribute of any class, you will find that it is indeed an
instance of a type object. In other words, a class is a type now

INSTANCE ATTRIBUTES
 An instance attribute is a Python variable belonging to only one object.
 It is only accessible in the scope of the object and it is defined inside the constructor
function of a class. For example, __init__(self,..).
 So Instance Attributes are unique to each object, (an instance is another name for an
object).
 Here, any Employee object we create will be able to store its name and age. We can
change attribute value of an object of employee, without affecting any other employee
objects we’ve created.
 The data values can be associated with a particular instance of any class and are
accessible via the familiar dotted-attribute notation.
 These values are independent of any other instance or of the class it was instantiated
from.
 When an instance is deallocated, so are its attributes.

"Instantiating" Instance Attributes (or Creating a Better Constructor):


 Instance attributes can be set any time after an instance has been created, in any piece of
code that has access to the instance.
 However, one of the key places where such attributes are set is in the constructor,
__init__().

Constructor First Place to Set Instance Attributes:


 The constructor is the earliest place that instance attributes can be set because __init__()
is the first method called after instance objects have been created.
 There is no earlier opportunity to set instance attributes.
 Once __init__() has finished execution, the instance object is returned, completing the
instantiation process.

Example on Class and Instance attributes:


class MainClass(object):
class_attr = 'CSE'
def __init__(self, instance_attr):
self.instance_attr = instance_attr
obj1 = MainClass('IOT')
obj2 = MainClass('IT')
# print the instance attributes
print (obj1.instance_attr)
print (obj2.instance_attr)
#print the class attribute using Mainclass
print(MainClass.class_attr)
#print the classattribute using objects
print (obj1.class_attr)
print (obj2.class_attr)
#printing instance attribute as a class property gives error
#print (MainClass.instance_attr)

Output:
IOT
IT
CSE
CSE
CSE

 If a constructor is defined, it should not return any object because the instance object is
automatically returned after the instantiation call.
 Correspondingly, __init__() should not return any object (or return None); otherwise,
there is a conflict of interest because only the instance should be returned.
 Attempting to return any object other than None will result in a TypeError exception:
>>> class MyClass:
... def __init__(self):
... print 'initialized'
... return 1 ...
>>> mc = MyClass()
initialized

Traceback (innermost last):


File "<stdin>", line 1, in ?
mc = MyClass()
TypeError: __init__() should return None

Determining Instance Attributes:


 The dir() built-in function can be used to show all instance attributes in the same manner
that it can reveal class attributes:
>>> class C(object):
... pass
>>> c = C()
>>> c.foo = 'roger'
>>> c.bar = 'shrubber'
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__',
'__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__',
'__weakref__', 'bar', 'foo']

 Similar to classes, instances also have a __dict__ special attribute (also accessible by
calling vars() and passing it an instance), which is a dictionary representing its attributes:
>>> c.__dict__ {'foo': 'roger', 'bar': 'shrubber'}

Special Instance Attributes


 Instances have only two special attributes .
 For any instance I:
Special Instance Attributes
 I.__class__ Class from which I is instantiated
 I.__dict__ Attributes of I
 We will now take a look at these special instance attributes using the class C and its
instance c:
>>>class C(object): # define class
... pass
...
>>> c = C() # create instance
>>> dir(c) # instance has no attributes
[]
>>> c.__dict__ # yep, definitely no attributes
{}
>>> c.__class__ # class that instantiated us
<class '__main__.C'>

 As you can see, c currently has no data attributes, but we can add some and recheck the
__dict__ attribute to make sure they have been added properly:
>>> c.foo = 1
>>> c.bar = 'SPAM'
>>> '%d can of %s please' % (c.foo, c.bar)
'1 can of SPAM please'
>>> c.__dict__
{'foo': 1, 'bar': 'SPAM'}
Instance Attributes versus Class Attributes:

BINDING AND METHOD INVOCATION


 Now we need to readdress the Python concept of binding, which is associated primarily
with method invocation. We will first review some facts regarding methods.
 First, a method is simply a function defined as part of a class.
 Second, methods can be called only when there is an instance of the class upon which the
method was invoked. When there is an instance present, the method is considered bound
(to that instance).
 Without an instance, a method is considered unbound.
 And finally, the first argument in any method definition is the variable self, which
represents the instance object invoking the method.

Invoking Bound Methods:


 If a function is an attribute of class and it is accessed via the instances, they are called
bound methods. A bound method is one that has ‘self‘ as its first argument. Since these
are dependent on the instance of classes, these are also known as instance methods.

Need for these bound methods


 The methods inside the classes would take at least one argument. Different instances of a
class have different values associated with them.
 For example, if there is a class “Fruits”, and instances like apple, orange, mango are
possible. Each instance may have different size, color, taste, and nutrients in it. Thus to
alter any value for a specific instance, the method must have ‘self’ as an argument that
allows it to alter only its property.
Example program to demonstrate bound and unbound methods
class sample(object):
# Static variable for object number
objectNo = 0
def __init__(self, name1):
# variable to hold name
self.name = name1
# Increment static variable for each object
sample.objectNo = sample.objectNo + 1
def myFunc(self):
print("My name is ", self.name,"from object ", self.objectNo)
def alterIt(self, newName):
self.name = newName
def myFunc2():
print("I am not a bound method !!!")

# creating first instance of class sample


samp1 = sample("A")
samp1.myFunc()
# unhide the line below to see the error
# samp1.myFunc2() #----------> error line because it is unbound method
# creating second instance of class sample
samp2 = sample("B")
samp2.myFunc()
samp2.alterIt("C")
samp2.myFunc()

Output:
My name is A from object 1
My name is B from object 2
My name is C from object 2
 In the previous example two instances namely samp1 and samp2 are created. Note that
when the function alterIt() is applied to the second instance, only that particular
instance’s value is changed.
 The line samp1.myFunc2() will be expanded as sample.myFunc2(samp1).
 For this method no explicit argument is required to be passed. The instance samp1 will be
passed as argument to the myFunc2(). The line samp1.myFunc2() will generate the error
:
Traceback (most recent call last):
File "C:/Python38/OOPS/bound_unbound.py", line 19, in <module>
samp1.myFunc2() #----------> error line
TypeError: myFunc2() takes 0 positional arguments but 1 was given

 It means that this method is unbound. It does not accept any instance as an argument.
These functions are unbound functions.

Invoking Unbound Methods:

Unbound methods and Static methods


 Methods that do not have an instance of the class as the first argument are known as
unbound methods.
 As of Python 3.0, the unbound methods have been removed from the language.
 They are not bounded with any specific object of the class. To make the
method myFunc2() work in the above class it should be made into a static method
 Static methods are similar to class methods but are bound completely to class instead of
particular objects.
 They are accessed using class names.

Need for making a method static


 Not all the methods need to alter the instances of a class. They might serve any common
purpose. A method may be a utility function also.
 For example, When we need to use certain mathematical functions, we use the built in
class Math.
 The methods in this class are made static because they have nothing to do with specific
objects. They do common actions. Thus each time it is not an optimized way to write as:
math=Math()
math.ceil(5.23)
 So they can be simply accessed using their class name as Math.ceil(5.23).

Static Method:
A method can be made static in two ways:
 Using staticmethod()
 Using decorator
Using staticmethod(): The staticmethod() function takes the function to be converted as its
argument and returns the static version of that function. A static function knows nothing about
the class, it just works with the parameters passed to it.
class Foo():
def bar(x):
return x + 5
Foo.bar = staticmethod(Foo.bar)
f=Foo()
print(f.bar(4))
Output: 9

Using decorators:
 These are features of Python used for modifying one part of the program using another
part of the program at the time of compilation.
 The decorator that can be used to make a method static is
@staticmethod
 This informs the built-in default metaclass not to create any bound methods for this
method.
 Once this line is added before a function, the function can be called using the class name.
 Now if I declare @staticmethod the self argument isn't passed implicitly as the first
argument
class Foo():
@staticmethod
def bar(x):
return x + 5
f=Foo()
print(f.bar(4))

Output:
9

COMPOSITION
 It is one of the fundamental concepts of Object-Oriented Programming.
 In this concept, we will describe a class that references to one or more objects of other
classes as an Instance variable.
 Here, by using the class name or by creating the object we can access the members of one
class inside another class.
 It enables creating complex types by combining objects of different classes.
 It means that a class Composite can contain an object of another class Component.
 This type of relationship is known as Has-A Relation.
 In the below figure Classes are represented as boxes with the class
name Composite and Component representing Has-A relation between both of them.
Example: Composite Class as Employee and Component Class as Salary

Example program to demonstrate composition:


class Salary:
def __init__(self, pay,bonus):
self.pay=pay
self.bonus=bonus
def get_total(self):
return(self.pay*12)+self.bonus
class Employee:
def __init__(self,e_id,name,age,pay,bonus):
self.e_id=e_id
self.name=name
self.age=age
self.obj_salary = Salary(pay,bonus)
def annual_salary(self):
return self.obj_salary.get_total()
e1 = Employee(145,"ABC",30,40000,10000)
print(e1.annual_salary())

Output:
490000

SUB CLASSING AND DERIVATION


 Composition works fine when classes are distinct and are a required component of larger
classes, but when you desire "the same class but with some tweaking," derivation is a
more logical option.
 One of the more powerful aspects of OOP is the ability to take an already defined class
and extend it or make modifications to it without affecting other pieces of code in the
system that use the currently existing classes.
 OOD allows for class features to be inherited by descendant classes or subclasses.
 These subclasses derive the core of their attributes from base (aka ancestor, super)
classes.
 Classes involved in a one-level derivation have a parent and child class relationship.
Those classes that derive from the same parent have a sibling relationship. Parent and all
higher-level classes are considered ancestors.

Creating Sub classes:


 The syntax for creating a subclass looks just like that for a regular (new-style) class, a
class name followed by one or more parent classes to inherit from:
class SubClassName (ParentClass1[, ParentClass2, ...]):
'optional class documentation string'
class_suite
 If your class does not derive from any ancestor class, use object as the name of the parent
class.
class Parent(object): # define parent class
def parentMethod(self):

Sub classes Example:


class Parent(object): # define parent class
def parentMethod(self):
print ('calling parent method' )
class Child(Parent): # define child class
def childMethod(self):
print ('calling child method' )
p = Parent() # instance of parent
p.parentMethod() #calling parent method using instance of parent
c = Child() # instance of child
c.childMethod() #calling child method using instance of child
c.parentMethod() # calling parent method using instance of child

INHERITANCE
Inheritance is the capability of one class to derive or inherit the properties from another class.
The benefits of inheritance are:
 It represents real-world relationships well.
 It provides reusability of a code. We don’t have to write the same code again and again.
Also, it allows us to add more features to a class without modifying it.
 It is transitive in nature, which means that if class B inherits from another class A, then
all the subclasses of B would automatically inherit from class A.
 So Inheritance models is called an is a relationship. This means that when you have
a Derived class that inherits from a Base class, you created a relationship
where Derived is a specialized version of Base.
 Inheritance is represented using the Unified Modeling Language or UML in the following
way:

 Classes are represented as boxes with the class name on top. The inheritance relationship
is represented by an arrow from the derived class pointing to the base class.
 The word extends is usually added to the arrow.

In an inheritance relationship:
 Classes that inherit from another are called derived classes, subclasses, or subtypes.
 Classes from which other classes are derived are called base classes or super classes.
 A derived class is said to derive, inherit, or extend a base class.
 Let’s say you have a base class Animal and you derive from it to create a Horse class.
 The inheritance relationship states that a Horse is an Animal.
 This means that Horse inherits the interface and implementation of Animal,
and Horse objects can be used to replace Animal objects in the application.
Sample Example:
class Parent():
def first(self):
print('first function')

class Child(Parent):
def second(self):
print('second function')

ob = Child()
ob.first()
ob.second()

Output:
first function
second function

 In the above program, you can access the parent class function using the child class
object.

__init__( ) Function:
 The __init__() function is called every time a class is being used to make an object.
 When we add the __init__() function in a parent class, the child class will no longer be
able to inherit the parent class’s __init__() function.
 The child’s class __init__() function overrides the parent class’s __init__() function.
Example to demonstrate init method in inheritance:
class Person:
def __init__(self,fname,age):
self.firstname=fname
self.age=age
def view(self):
print(self.firstname,self.age)
class Employee(Person):
def __init__(self,fname,age):
Person.__init__(self,fname,age)
self.lastname ="Harryson"
def view(self):
print("First Name:" , self.firstname)
print("Last Name:" , self.lastname)
print("Age:" , self.age)
ob = Employee("John" , '28')
ob.view()

Output:
First Name: John
Last Name: Harryson
Age: 28

Types of Inheritance:
Inheritance is categorized based on the hierarchy followed and the number of parent classes and
subclasses involved.
There are five types of inheritances:
 Single Inheritance
 Multiple Inheritance
 Multilevel Inheritance
 Hierarchical Inheritance
 Hybrid Inheritance

Single Inheritance:
 This type of inheritance enables a subclass or derived class to inherit properties and
characteristics of only one parent class, this avoids duplication of code and improves
code reusability.
Single Inheritance Example:
class Parent:
def func1(self):
print("This is function 1")
class Child(Parent):
def func2(self):
print("This is function 2 ")
ob = Child()
ob.func1()
ob.func2()
Output:
This is function 1
This is function 2

Multiple Inheritance:
 This inheritance enables a child class to inherit from more than one parent class.
 This type of inheritance is not supported by java classes, but python does support this
kind of inheritance.
 It has a massive advantage if we have a requirement of gathering multiple characteristics
from different classes.

Multiple Inheritance Example:


class Parent1:
def func1(self):
print("this is function 1")
class Parent2:
def func2(self):
print("this is function 2")
class Child(Parent1 , Parent2):
def func3(self):
print("this is function 3")
ob = Child()
ob.func1()
ob.func2()
ob.func3()

Output:
this is function 1
this is function 2
this is function 3

Multilevel Inheritance
 In multilevel inheritance, the transfer of the properties of characteristics is done to more
than one class hierarchically.
 To get a better visualization we can consider it as an ancestor to grandchildren relation or
a root to leaf in a tree with more than one level.

Multilevel Inheritance Example:


class GrandParent:
def func1(self):
print("this is function 1")
class Parent(GrandParent):
def func2(self):
print("this is function 2")
class Child(Parent):
def func3(self):
print("this is function 3")
ob = Child()
ob.func1()
ob.func2()
ob.func3()

Output:
this is function 1
this is function 2
this is function 3

Hierarchical Inheritance
 This inheritance allows a class to host as a parent class for more than one child class or
subclass.
 This provides a benefit of sharing the functioning of methods with multiple child classes,
hence avoiding code duplication.

Hierarchical Inheritance Example:


class Parent:
def func1(self):
print("this is function 1")
class Child1(Parent):
def func2(self):
print("this is function 2")
class Child2(Parent):
def func3(self):
print("this is function 3")
ob1 = Child1()
ob2 = Child2()
ob1.func1()
ob1.func2()
ob2.func1()
ob2.func3()

Output:
this is function 1
this is function 2
this is function 1
this is function 3

Hybrid Inheritance
 An inheritance is said hybrid inheritance if more than one type of inheritance is
implemented in the same code.
 This feature enables the user to utilize the feature of inheritance at its best.
 This satisfies the requirement of implementing a code that needs multiple inheritances in
implementation.

Hybrid Inheritance Example:


class Parent:
def func1(self):
print("this is function one")

class Child1(Parent):
def func2(self):
print("this is function 2")

class Child2(Parent):
def func3(self):
print(" this is function 3")

class Child3(Child1,Child2):
def func4(self):
print(" this is function 4")

ob = Child3()
ob.func1()

Output:
this is function one

BUILT-IN FUNCTIONS FOR CLASSES, INSTANCES,


AND OTHER OBJECTS
issubclass():
 The issubclass() Boolean function determines if one class is a subclass or descendant of
another class.
 It has the following syntax:
issubclass(sub, sup)
 issubclass() returns True if the given subclass sub is indeed a subclass of the superclass
sup (and False otherwise).
 This function allows for an "improper" subclass, meaning that a class is viewed as a
subclass of itself, so the function returns true if sub is either the same class as sup or
derived from sup. (A "proper" subclass is strictly a derived subclass of a class.)
 The second argument of issubclass() can be tuple of possible parent classes for which it
will return true if the first argument is a subclass of any of the candidate classes in the
given tuple.

isinstance():
 The isinstance() Boolean function is useful for determining if an object is an instance of a
given class.
 It has the following syntax:
isinstance(obj1, obj2)
 isinstance() returns True
if obj1 is an instance of class obj2 or is an instance of a subclass of obj2 (and False
otherwise), as indicated in the following examples:
>>> class C1(object):
pass
>>> class C2(object):
pass
>>> c1 = C1()
>>> c2 = C2()
>>>isinstance(c1, C1)
True
>>>isinstance(c2, C1)
False
>>>isinstance(c1, C2)
False

.hasattr(), getattr(), setattr(), delattr():


 The *attr() functions can work with all kinds of objects, not just classes and instances.
 However, since they are most often used with those objects, we present them here.
 One thing that might throw you off is that when using these functions, you pass in the
object you are working on as the first argument, but the attribute name, the second
argument to these functions, is the string name of the attribute.
 In other words, when operating with obj.attr, the function call will be like *attr(obj,
'attr'...)this will be clear in the examples that follow.
 The hasattr() function is Boolean and its only purpose is to determine whether or not an
object has a particular attribute, presumably used as a check before actually trying to
access that attribute.
 The getattr() and setattr() functions retrieve and assign values to object attributes,
respectively.
 getattr () will raise an AttributeError exception if you attempt to read an object that does
not have the requested attribute, unless a third, optional default argument is given.
 setattr() will either add a new attribute to the object or replace a pre-existing one.
 The delattr() function removes an attribute from an object.

>>> class myClass(object):


... def __init__(self):
... self.foo = 100
...
>>> myInst = myClass()
>>>hasattr(myInst, 'foo')
True
>>>getattr(myInst, 'foo')
100
>>>hasattr(myInst, 'bar')
False
>>>getattr(myInst, 'bar')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
getattr(myInst, 'bar')
AttributeError: myClass instance has no attribute 'bar'

>>>setattr(myInst, 'bar', 'my attr')


>>> dir(myInst)
['__doc__', '__module__', 'bar', 'foo']

>>>getattr(myInst, 'bar') # same as myInst.bar


'my attr'
>>>delattr(myInst, 'foo')
>>> dir(myInst)
['__doc__', '__module__', 'bar']
>>>hasattr(myInst, 'foo')
False
dir():
 dir() is a powerful inbuilt function in Python3, which returns list of the attributes and
methods of any object (say functions , modules, strings, lists, dictionaries etc.)
 dir() on an instance shows the instance variables as well as the methods and class
attributes defined by the instance's class and all its base classes.
 dir() on a class shows the contents of the __dict__ of the class and all its base classes.
 It does not show class attributes that are defined by a metaclass.
 dir() on a module shows the contents of the module's __dict__.
 dir() without arguments shows the caller's local variables.

super():
 The super() function in Python makes class inheritance more manageable and
extensible. The function returns a temporary object that allows reference to a parent class
by the keyword super.
The super() function has two major use cases:
 To avoid the usage of the super (parent) class explicitly.
 To enable multiple inheritances.

Program using super():


class Person(object):
def __init__(self, personName):
print(personName, 'is a nice person')

class Employee(Person):
def __init__(self):
print('John is working in a good company')
super().__init__('John')

e1 = Employee()
Output:
John is working in a good company
John is a nice person

vars():
 This is an inbuilt function in Python.
 The vars() method takes only one parameter and that too is optional.
 It takes an object as a parameter which may be can a module, a class, an instance, or
any object having __dict__ attribute.

Syntax:
vars(object)

 The method returns the __dict__ attribute for a module, class, instance, or any other
object if the same has a __dict__ attribute.
 If the object fails to match the attribute, it raises a TypeError exception.
 Objects such as modules and instances have an updateable __dict__ attribute however,
other objects may have written restrictions on their __dict__ attributes.
 vars() acts like locals() method when an empty argument is passed which implies that
the locals dictionary is only useful for reads since updates to the locals dictionary are
ignored.

class C(object):
pass
>>> c = C()
>>> c.foo = 100
>>> c.bar = 'Python'
>>> c.__dict__
{'foo': 100, 'bar': 'Python'}
>>> vars(c)
{'foo': 100, 'bar': 'Python'}
TYPES VS. CLASSES/INSTANCES
 A user-defined class (or the class "object") is an instance of the class "type".
 So, we can see, that classes are created from type. In Python3 there is no difference
between "classes" and "types". They are in most cases used as synonyms.
 The fact that classes are instances of a class "type" allows us to program meta classes.
>>> c=“abc"
>>> type(c)
<class 'str'>
>>> type(str)
<class 'type'>
>>> class d:
pass
>>> type(d)
<class 'type'>
 We can create classes, which inherit from the class "type". So, a metaclass is a subclass
of the class "type".
 Instead of only one argument, type can be called with three parameters:
type(classname, superclasses, attributes_dict)
 If type is called with three arguments, it will return a new type object. This provides us
with a dynamic form of the class statement.
 "classname" is a string defining the class name and becomes the name attribute;
 "superclasses" is a list or tuple with the superclasses of our class. This list or tuple will
become the bases attribute;
 the attributes_dict is a dictionary, functioning as the namespace of our class. It contains
the definitions for the class body and it becomes the dict attribute.
class A:
pass
x = A()
print(type(x))
<class '__main__.A'>

 We can use "type" for the previous class defintion as well:


A = type("A", (), {})
x = A()
print(type(x))
<class '__main__.A‘>
 Generally speaking, this means, that we can define a class A with type(classname,
superclasses, attributedict)
 The class definitions for A and A1 are syntactically completely different, but they
implement logically the same class.
CUSTOMIZING CLASSES WITH SPECIAL METHODS
 Generally the important aspect of methods is that it must be bound (to an instance of their
corresponding class) before they can be invoked; and second, that there are two special
methods which provide the functionality of constructors and destructors, namely
__init__() and __del__() respectively.
 In fact, __init__() and __del__() are part of a set of special methods which can be
implemented
 Some have the predefined default behavior of inaction while others do not and should be
implemented where needed.
 These special methods allow for a powerful form of extending classes in Python.
 In particular, they allow for:
1. Emulating standard types
2. Overloading operators
 Special methods enable classes to emulate standard types by overloading standard
operators such as +, * ,these methods begin and end with a double underscore ( __ ).

add_dunder demo program:


class Employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __add__(self,others):
return self.salary + others.salary
e1 = Employee("ABC",10000)
e2 = Employee("XYZ",12000)
print(e1+e2)

Output:
22000
PRIVACY

 Attributes in Python are, by default, "public" all the time, accessible by both code within
the module and modules that import the module containing the class.
 Many OO languages provide some level of privacy for the data and provide only accessor
functions to provide access to the values. This is known as implementation hiding and is
a key component to the encapsulation of the object.
 Most OO languages provide "access specifiers" to restrict access to member functions.
 Access specifiers are used to restrict or control the accessibility of class resources, by
declaring them as public, private and protected.
 In python we follow some convention to declare members as public, private and
protected.
 There are no such keywords like public, protected and private in python like other OOP
languages.

Public:
 Public members are available publicly, means can be accessed from any where.
 By default, every member of class is public in Python.

Demo_public program:
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
def display(self):
print("Age is:",self.age)

p1 = Person("ABC",50)
p1.display()
print("Age is:",p1.name)

Output:
Age is: 50
Age is: ABC

Private (Double Underscore ( __ )):


 Python provides an elementary form of privacy for class elements (attributes or methods).
 Attributes that begin with a double underscore (__) are mangled during runtime so direct
access is thwarted.
 In actuality, the name is prep ended with an underscore followed by the class name.
 Although this provides some level of privacy, the algorithm is also in the public domain
and can be defeated easily. It is more of a protective mechanism for importing modules
that do not have direct access to the source code or for other code within the same
module.
 The other purpose of this type of name-mangling is to protect __XYZ variables from
conflicting with derived class namespaces.
 If you have an __XYZ attribute in a class, it will not be overridden by a child class's
___XYZ attribute.
 By using __XYZ, the code for the child class can safely use __XYZ without worrying
that it will use or affect __XYZ in the parent.
Demo_private Program:
class Person:
def __init__(self,name,age):
self.__name = name #private instance attribute
self.__age = age #private instance attribute
def __display(self): #private instance method
print("Age is:",self.__age)

p1 = Person("ABC",50)
p1.__display()
print("Age is:",p1.__name)

Output:
Traceback (most recent call last):
File "C:\Python38\OOPS\public.py", line 9, in <module>
p1.__display()
AttributeError: 'Person' object has no attribute '__display'

Name mangling in Python:


 Python does not have any strict rules when it comes to public, protected or private, like
java.
 So, to protect us from using the private attribute in any other class, Python does name
mangling, which means that every member with a double underscore will be changed to
_object._class__variable when trying to call using an object.
 The purpose of this is to warn a user, so he does not use any private class variable or
function by mistake without realizing its states.
 The use of single underscore and double underscore is just a way of name mangling
because Python does not take the public, private and protected terms much seriously so
we have to use our naming conventions by putting single or double underscore to let the
fellow programmers know which class they can access or which they can't.

Demo_name_mangling Program:
class Person:
def __init__(self,name,age):
self.__name = name #private instance attribute
self.__age = age #private instance attribute
def __display(self):
print("Age is:",self.__age)

p1 = Person("ABC",50)
print("Age is:",p1._Person__age)

Output:
Age is: 50

Protected (Single Underscore ( _ )):


 Simple module-level privacy is provided by using a single underscore ( _ ) character
prefixing an attribute name.
 Here, its members and functions can only be accessed by the classes derived from it, i.e.
its child class or classes.
 This prevents a module attribute from being imported with "from mymodule import *".
This is strictly scope-based, so it will work with functions too.
 Although Python does not have syntax built into the language that has the flavors of
private, protected, friend, or protected friend, you can customize access in the exact way
that fits your needs.
Demo_protected Program:
class Person:
def __init__(self,name,age):
self._name = name #Protected instance attribute
self._age = age #Protected instance attribute
def _display(self):
print("Person name is:",self._name)
class Employee(Person):
pass

p1 = Person("ABC",50)
print("Age is:",p1._age)
p1._display()

Output:
Age is: 50
Person name is: ABC

DECORATORS
 Python has an interesting feature called decorators to add functionality to an existing
code.
 A python decorator is a function that takes in a function, add some functionality to it and
returns the original function.
 So decorators are very powerful and useful tool in Python since it allows programmers to
modify the behavior of function or class.
 It allow us to wrap another function in order to extend the behavior of the wrapped
function, without permanently modifying it.
 As stated above the decorators are used to modify the behavior of function or class.
 This is also called metaprogramming because a part of the program tries to modify
another part of the program at compile time.
 In Decorators, functions are taken as the argument into another function and then called
inside the wrapper function.

Syntax for Decorator:


@gfg_decorator
def hello_decorator():
print("Gfg")
'''Above code is equivalent to -
def hello_decorator():
print("Gfg")
hello_decorator = gfg_decorator(hello_decorator)'''

Demo program on decorators:


def my_decorator(func):
def wrapper():
print("Python Programming ")
func()
print("Concept of Python Programming")
return wrapper
def display_module():
print("OOP")
display_module = my_decorator(display_module)
display_module()
Output:
Python Programming
OOP
Concept of Python Programming

Demo program on decorators:


def my_decorator(func):
def wrapper():
print("Python Programming ")
func()
print("Concept of Python Programming")
return wrapper
@my_decorator
def display_module():
print("OOP")
display_module()
Output:
Python Programming
OOP
Concept of Python Programming

Decorating Functions with Parameters:


 The above decorator was simple and it only worked with functions that did not have any
parameters.
 If we had functions that took in parameters also it will uses those parameters and can use
decorators.
 We need to ensure that the parameters of the nested inner() function inside the decorator
is the same as the parameters of functions it decorates.
Demo Program:
def employee(func):
def inner(name):
print("Employee Name is:",name)
return func(name)
return inner
@employee
def employee_detail(name):
return name
employee_detail("John")

Output:
Employee Name is: John

Chaining decorators in Python:


 Multiple decorators can be chained in Python.
 This is to say, a function can be decorated multiple times with different (or same)
decorators. We simply place the decorators above the desired function.

Demo Program:
def dollar(func):
def inner(arg):
print("$" * 35)
func(arg)
print("$" * 35)
return inner
def star(func):
def inner(arg):
print("*" * 35)
func(arg)
print("*" * 35)
return inner
@dollar
@star
def printer(msg):
print(msg)
printer("This is chaining in decorators")
Output:
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
***********************************
This is chaining in decorators
***********************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

The given syntax of,


@dollar
@star
def printer(msg):
print(msg)
is equivalent to
def printer(msg):
print(msg)
printer = dollar(star(printer))

The order in which we chain decorators matter. If we had reversed the order as,
@star
@dollar
def printer(msg):
print(msg)

Output:
***********************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
This is chaining in decorators
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
***********************************

You might also like