Python OOPs Concepts

What is Object-oriented programming(OOP)?

Python is an object-oriented programming language. Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain attributes(data) and behaviors. In other words, Object-oriented programming is a means of structuring a program, so that the data and its behavior (methods) are bundled in a single location(object). It makes it easier to understand how a program works. Object-oriented programming focuses on the code reusability.

Object-Oriented Vs Procedural Oriented programming:

Object Oriented ProgrammingProcedural Oriented Programming
Follows Bottom-Up approachFollows Top-Down approach
Program is divided into small parts called objectsProgram is divided into small parts called Functions
Simulates real world entitiesDoes not simulate real-world entities
Provides data hiding, hence its more secureDoes not have any proper way to hide the data. Hence it's less secure
Easier to extend the code by adding new functions and dataNot easy to add new functions and data
Examples: C,FORTRAN,VB,etc.,Examples: C++, Java, Python,etc.,

Python OOPs Concepts:

The major principles of object-oriented programming are:

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

Python OOPs Concepts-Class:

A class is a blueprint for the objects. We can also define the class as a collection of objects. It is a logical entity that has some specific attributes and methods. For example, for class “Employee”, the attributes and behaviors are the name, age, designation, calculate_salary, etc.,

How to create a Class?

In Python, class definition starts with the “class” keyword, followed by the name of the class. The code indented below the class definition is the body of the class. In Python, the class names start with a Capital letter by convention.

Syntax:

class Employee:
   pass

Example:

class Car:

    def __init__(self,model,year,color):
        self.model=model
        self.year=year
        self.color=color

    def brake(self):
        pass

    def accelerate(self):
        pass

    def gear_change(self):
        pass

    def print_car_details(self):
        print("{} is of {} color and made in year {}".format(self.model,self.color,self.year))

__init__ method:

The __init__() is nothing but a constructor in Python. The constructor initializes the state of the object. The attributes present inside the __init__ method are known as instance attributes. The purpose of the constructor is to initialize or assign values to the instance attributes when an object is created. The instance attributes are specific to the particular instance(object) of the class. The __init__ method runs as soon as an object is created for a class. In our example, each instance of a class “Car” has a specific model, year, and color.

Default Constructor:

What if we forgot to declare the __init__ method in our class? Python does it for us when we don’t declare a constructor in our program. The default constructor does not accept any arguments. Here is an example of a default constructor.

class Employee:
    emp_id=101

    def display(self):
        print(self.emp_id)

emp=Employee()
emp.display()
101

self parameter:

The ‘self’ parameter refers to the current instance of a class. The object itself passed as the first parameter of any method present inside the class. To represent object we use ‘self’ by convention. But it can be anything.

class Sample():
    var=10

    def display(obj):
        print(obj.var)

s=Sample()
s.display()

In this example, instead of ‘self’, we are using ‘obj’ and we get the desired output.

10

Python OOPs Concepts-Object:

The object is an instance of a class. The object represents the real-word entities like book, house, pencil, etc. An object has two characteristics:

  • Attributes(data)
  • Behavior

For instance, let’s take the Car as an object. It has the following attributes and behaviors:

Attributes: Model, Year, Color

Behavior: Brake, Accelerate, Gear Change.

How to create an object?

We instantiate an object by calling the class name.

Syntax:

c1=Car("Toyota",2015,"White")
>>> c1
<__main__.Car object at 0x0302CB10>

Example:

c1=Car("Toyota",2015,"White")
c2=Car("Renault",2017,"Blue")
c1.print_car_details()
c2.print_car_details(
Toyota is of White color and made in year 2015
Renault is of Blue color and made in year 2017

In this example, we have created objects c1 and c2 for class “Car” to access the class attributes. Each object has a separate memory location.

>>> c1==c2
False

Delete attributes and objects:

Using the ‘del’ keyword we can delete the attributes of an object and the object itself.

>>> del c1.model
>>> c1.print_car_details()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "sample.py", line 18, in print_car_details
    print("{} is of {} color and made in year {}".format(self.model,self.color,self.year))
AttributeError: 'Car' object has no attribute 'model'
>>> del c1
>>> c1.print_car_details()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c1' is not defined

Class and instance attributes:

So far we have seen how to create a class, instance for a class, constructor, and how to delete an object and its attributes. As mentioned before, the attributes defined inside the __init__() are specific to an object and known as instance attributes. The attributes defined outside the __init__() are class attributes. The instance attribute can be accessed only by an object. The class attribute can be accessed by both class and objects, as it is shared between all of them.

You might get a question about why we need  class attributes. Whenever the __init__() is called, it initializes the instance attributes. You might want some attributes not to lose its current state, every time we create a new object. For instance, let’s say there are a certain number of bananas. Assume that each monkey eats one banana. You want to print the number of bananas after each monkey eats.

class Monkey():

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

    def eat(self):
        self.No_of_bananas-=1

    def display(self):
        print("Remaining Bananas :" +str(self.No_of_bananas))

No_of_bananas=10
m1=Monkey(No_of_bananas)
m2=Monkey(No_of_bananas)
m3=Monkey(No_of_bananas)
m1.eat()
m1.display()
m2.eat()
m2.display()
m3.eat()
m3.display()
Remaining Bananas :9
Remaining Bananas :9
Remaining Bananas :9

This is not the result we want right? The bananas count has to reduce after each monkey eats. That’s where the class attribute comes into place, which will not lose its current state every time a new object is created.

Example: Accessing class attribute using the class name:

class Monkey():
    #class attributes
    No_of_bananas=10
    monkey_eats_banana=1

    def eat(self):
        Monkey.No_of_bananas-=Monkey.monkey_eats_banana

    def display(self):
        print("Remaining Bananas :" +str(Monkey.No_of_bananas))

m1=Monkey()
m2=Monkey()
m3=Monkey()
m1.eat()
m1.display()
m2.eat()
m2.display()
m3.eat()
m3.display()
Remaining Bananas :9
Remaining Bananas :8
Remaining Bananas :7

Here, we are accessing the class attributes using a class name and it doesn’t lose its value, for every object created.

Example: Accessing class attribute using the objects:

What if some monkeys eat more than one banana. We can change the class attribute for a specific object alone.

class Monkey():
    #class attributes
    No_of_bananas=10
    monkey_eats_banana=1

    def eat(self):
        Monkey.No_of_bananas-=self.monkey_eats_banana

    def display(self):
        print("Remaining Bananas :" +str(Monkey.No_of_bananas))

m1=Monkey()
m2=Monkey()
m1.monkey_eats_banana=2
m1.eat()
m1.display()
m2.eat()
m2.display()
Remaining Bananas :8
Remaining Bananas :7

Python OOPs Concepts-Method:

The method defines the behavior of an object. The method is nothing the functions defined inside the body of the class. A method is not unique to the class instances. Python has 3 different methods:

  • Instance method
  • Class method
  • Static method
class Myclass:

    def instance_method(self):
        pass

    @classmethod
    def class_method(cls):
        pass

    @staticmethod
    def static_method():
        pass

Let’s see each of these methods in detail.

Instance method:

Instance methods are most commonly used. It receives the instance of the class(object) which is self by convention, as the first parameter. The self has access to both object attributes as well as the class attributes.

Class method:

We define the class method with the decorator @classmethod. It accepts class as the first parameter which is ‘cls’ by convention. The cls has access to the class attributes which is shared across all the instances of the class. The cls does not access to the object attributes.

Static method:

We define the status method with the decorator @staticmethod. It doesn’t accept self or cls as an argument. Hence the static method has access to neither the class attributes nor the object attributes. It acts like a normal function but belongs to the namespace of the class.

Example:

from datetime import date

class Car:
    def __init__(self,model,year,color):
        self.model=model
        self.year=year
        self.color=color

    @classmethod
    def Toyota(cls):
        return cls("Toyota",2015,"White")

    @staticmethod
    def find_age(year):
        return date.today().year-year

    def display(self):
        age=Car.find_age(self.year)
        print('{} is of {} color and {} years old'.format(self.model,self.color,age))

c1=Car("Renault",2017,"Blue")
c1.display()
c2=Car.Toyota()
print(c2)
c2.display()
Renault is of Blue color and 3 years old
<__main__.Car object at 0x0549CD90>
Toyota is of White color and 5 years old

From the above example, we can conclude:

  • The ‘Car.Toyota()’ creates a class object.
  • The static method acts like a regular function. But it belongs to the class namespace and works on the data given in the arguments.

Python OOPs Concepts-Inheritance:

Inheritance enables us to define a class that derives or inherits the properties from another class. The new class is known as derived or base class, the class from which the child class inherits is known as Parent class. The main benefit of inheritance is code-reusability. And we can also add new features to the child class if required, without modifying the parent class.

Syntax:

class childclass_name(parentclass_name)

Example:

We create two classes: Person and Employee. Since an employee is also a person we create employee class as a child class of person.

class Person:
    def __init__(self,name):
        self.name=name
    def is_empolyee(self):
        return False
    def display(self):
        return self.name

class Employee(Person):
    def is_empolyee(self):
        return True

obj1=Person("Person1")
print(obj1.display(),obj1.is_empolyee())
obj2=Employee("Person2")
print(obj2.display(),obj2.is_empolyee())
Person1 False
Person2 True

Even though __init__ and display methods are part of only the base class, the child class can access these methods. If the same method is present in both child and parent class, then the child class is given priority.

Types of Inheritance:

Single Inheritance:

If a child class inherits from only one parent, then it is known as single inheritance. The example which we saw above is an example of single inheritance.

Multiple Inheritance:

Unlike Java and C++, Python supports multiple inheritance.

When a child class inheritance from multiple base classes, then it is known as multiple inheritance. Here the parent classes are mentioned inside the parenthesis separated by a comma.

class Base1:
    def __init__(self):
        self.str1='Base class 1'

class Base2:
    def __init__(self):
        self.str2='Base class 2'

class Derived(Base1,Base2):
    def __init__(self):
        Base1.__init__(self)
        Base2.__init__(self)
    def display(self):
        print(self.str1,self.str2)

d=Derived()
d.display()
Base class 1 Base class 2

To access the instance variables of the parent class in the child class, we need to call the __init__() of the parent class.

Multilevel Inheritance:

Multilevel inheritance represents a parent-grandchild relationship. A child class inheritance from a parent class and a grandchild class inherits from a child class.

class Parent:
    def __init__(self):
        self.str1='Parent class'
        print(self.str1)

class Child:
    def __init__(self):
        Parent.__init__(self)
        self.str2='Child class'
        print(self.str2)

class GrandChild(Child):
    def __init__(self):
        Child.__init__(self)
    def display(self):
        print("Sub class of child class")

d=GrandChild()
d.display()
Parent class
Child class
Sub class of child class

Hierarchical Inheritance:

Single parent class has more than one child class.

Hybrid Inheritance:

It is a combination of more than one type of inheritance.

super() keyword:

We can refer to the base class object using the super keyword. It allows us to avoid using the base class name explicitly. The advantage is whenever a base class name is changed, we can reduce the number of places the name has to be changed if we refer to the base class object using the super() keyword.

class Parent1:
    def __init__(self):
        self.str1='Parent class 1'
    def display(self):
        print(self.str1)

class Parent2:
    def __init__(self):
        self.str2='Parent class2'
    def display(self):
        print(self.str2)

class Child(Parent2,Parent1):
    def display(self):
        print("Child class")
        super().display()

c=Child()
c.display()
Child class
Parent class2

When we have more than one parent class, the super() keyword refers to the base class that appears first in the parenthesis.

Python OOPs Concepts-Polymorphism:

The word Polymorphism is from the Greek words Poly(many) and morphism(forms). In Programming, Polymorphism means the ability of an object to take many forms and process each of these objects differently based on their data type.

Examples:

In-built polymorphic function:

The usage of len function is to return the length of the argument it accepts, irrespective of the type of the argument. The type of the argument can be string, tuple, list, etc., Hence its an example of polymorphism.

print(len('abcdgf'))
print(len([1,2,'a','m','n']))
print(len(('p','1',[1,2,3],'q')))
6
5
4

Polymorphism with function and objects:

class Carrot():
    def type(self):
        print('Vegetable')
    def color(self):
        print('Orange')

class Mango():
    def type(self):
        print('Fruit')
    def color(self):
        print('Yellow')

def func(obj):
    obj.type()
    obj.color()


c=Carrot()
m=Mango()
func(c)
func(m)
Vegetable
Orange
Fruit
Yellow

In the above example, we have a function func(), which accepts an object as an argument. Irrespective of the type of the object, func() calls the method of the object and returns the correct result. Here, we are assuming that the methods we are calling inside the func(), exists in all the classes.

Polymorphism with Inheritance:

Let’s consider an example. We have a base class ‘Shape’, which has a method ‘calculate_area’. The base class has two child classes: Circle and Square. In Inheritance, the child class inherits the methods of the base class. But the formula to calculate area varies according to the shape. In case, we apply the same formula to calculate the area for all the child classes, we won’t get the correct result. Polymorphism lets us define methods in child classes with the same name as the base class. Hence we can re-define the method ‘calculate_area’ in each of the child classes. This process of re-defining the method in the child class is known as Method overriding.

import math

class Shape:
    def calculate_area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius=radius
    def calculate_area(self):
        return math.pi*self.radius**2

class Square(Shape):
    def __init__(self, side):
        self.side=side
    def calculate_area(self):
        return self.side**2

c=Circle(3)
s=Square(4)
print(c.calculate_area())
print(s.calculate_area())
28.274333882308138
16

Python OOPs Concepts-Abstraction:

Abstraction means hiding the complexity and showing only the essential features of the object. Hence, in this way the user knows what he is doing but not how the work is done.

For instance, a user drives a vehicle without knowing what’s happening underneath.

In Python, we achieve abstraction using Abstract classes and Interface.

Interface:

An interface provides the methods without method bodies. For the methods defined in the interface, the child classes provide the implementation. Since we cannot create Interface explicitly in Python, we have to use an abstract class.

Abstract Class:

A class that contains one or more abstract methods is known as an Abstract class. An abstract method is a method that has only the declaration without any implementation. All the child classes built from the abstract class must contain these abstract methods. The child classes provide the implementation for the abstract methods. We can also define the abstract class as a blueprint for the other classes. We use abstract class when we have to design large functional units, provide a common interface for different implementations of a component, etc.,

A class that contains only the abstract methods is known as Interface.

Abstract Base Class(ABC) in Python:

In Python, the module abc provides the base to define the Abstract Base Classes(ABC). We define an abstract method by decorating the method with @abstractmethod.

from abc import ABC, abstractmethod

class Payment(ABC):
    def payment_amount(self, amount):
        print("You need to pay amount : {}".format(amount))

    #abstract method
    @abstractmethod
    def payment(self):
        pass

class CreditCardPayment(Payment):
    #overriding abstract method
    def payment(self):
        print('Amount successfully paid uisng credit card')

class MobileWalletPayment(Payment):
    #overriding abstract method
    def payment(self):
        print('Amount successfully paid uisng Mobile wallet')

c=CreditCardPayment()
c.payment_amount(1000)
c.payment()
m=MobileWalletPayment()
m.payment_amount(1050)
m.payment()
You need to pay amount : 1000
Amount successfully paid uisng credit card
You need to pay amount : 1050
Amount successfully paid uisng Mobile wallet

Instantiate abstract class:

We cannot create an object for an abstract class because it is incomplete. Abstract class contains abstract methods that have no implementation. Hence we have to use abstract class only as a template and we can extend it depending on the need.

from abc import ABC, abstractmethod

class Payment(ABC):
    def payment_amount(self, amount):
        print("You need to pay amount : {}".format(amount))

    #abstract method
    @abstractmethod
    def payment(self):
        pass

p=Payment()
Traceback (most recent call last):
  File "sample.py", line 12, in <module>
    p=Payment()
TypeError: Can't instantiate abstract class Payment with abstract methods payment

Concrete methods in Abstract base class:

A concrete class contains only normal(concrete) methods. whereas an abstract class contains both normal and concrete methods. An abstract base class that contains concrete methods can provide the implementation and we have to invoke them via super() keyword.

from abc import ABC

class Parent(ABC):
    #Concrete method
    def display(self):
        print("This is an Abstract Base Class")

class Child(Parent):
    def display(self):
        super().display()
        print("This is a child class")

c=Child()
c.display()
This is an Abstract Base Class
This is a child class

Python OOPs Concepts-Encapsulation:

Encapsulation is the idea of holding data and methods within one unit. This prevents the modification of data accidentally by restricting access to data and methods directly. In order to achieve this, we can declare the variables either as

  • private
  • protected

private variables:

When we declare a variable as private, then only the object’s method can access the object’s variable. Private variables are denied access outside the class. In Python, we declare the private variable, by prefixing the variable name with double underscore ‘__’.

protected variables:

Protected variables can be accessed by the class and its sub-classes. In Python, we declare the protected variable, by prefixing the variable name with single underscore ‘_’.

Real-life Scenario:

Let’s consider a real-life example of encapsulation. In a company, there are different departments like finance, accounting, sales, etc., Each of these departments deals with their own data. For instance, the finance department handles the finance data and the sales department handles the sales data. Let’s assume, there is a situation when a person from finance departments needs the sales data. He cannot access the sales-related data directly. He has to contact a person from the sales department in order to get access to the sales data. This is where the encapsulation comes. Using encapsulation, we wrap the data and employees of the sales department under a single class. Hence, the sales data can not be accessed by anyone outside that class.

Example – protected variable:

A class and its sub-classes can access the protected variable.

class Base:
    def __init__(self):
        #protected variable
        self._a=10

class Derived(Base):
    def __init__(self):
        Base.__init__(self)
        print(self._a)

b=Base()
d=Derived()
10

Example – private variable:

One cannot access a private variable outside the class.

class Base:
    def __init__(self):
        #private varaible
        self.__a=10

class Derived(Base):
    def __init__(self):
        Base.__init__(self)
        print(self.__a)

b=Base()
d=Derived()
Traceback (most recent call last):
  File "sample.py", line 12, in <module>
    d=Derived()
  File "sample.py", line 9, in __init__
    print(self.__a)
AttributeError: 'Derived' object has no attribute '_Derived__a'

 

Translate »