Object oriented programming (OOP) in Python

Object oriented programming (OOP) is a design pattern for creating stable applications and powerful systems. It allows handling rapid changes in complex systems easier. Classes are a blueprint of needed functionalities which is described by attributes and methods. By creating an object (instance) from the class, you can have access to class functionalities. You can create more than one object from the class.

Advantages of using OOP:

  • More reusable and shareable code: able to use the class and subclasses multiple times without rewriting everything from scratch
  • More security: you can hide and protect sensible attributes which should not be accessible & modified
  • Easy troubleshooting: Since you have to separate various functionalities in the classes, when you encounter an error, you know at least where to start for debugging
  • Reusability: through inheritance, you can reuse functionalities of multiple classes without recreating the wheel and when you apply a modification in the class, the change is propagating in all of the dependent classes & objects

The main keywords to learn in OOP are Class, Object, Methods, and Attributes. There are also 4 fundamental OOP concepts, which are: Inheritance, Encapsulation, Polymorphism, and Data abstraction. I will go through all of them in this post.

Start with the principal!

Everything is started with classes, which is a user-defined data type structure. Inside the class, there are methods and attributes. Methods are functions which are talking about actions and attributes are characteristics of the class.
Imagine, Car as a class, with main attributes such as model, price, color, and build year. We can create as many objects we want from the class.

Let’s go for a practical example in python. I want to create a class of employee, the main attributes are name, phone, email, and id. Methods can be any action related to the attributes, such as printing details of an employee or updating attributes values. Let’s implement this in python:

Class & attributes

class employee:
    pass

a = employee()

Here I have written the main body of class. An object of class is created by calling the class name, this process is called instantiation. You may have heard another term for object: instance, which is almost a similar concept. See it in this way, that object of a class is an instance of the class.

print(a)

<__main__.employee object at 0x000001F820981CA0>

When you print the object, what we can see is the class name and memory address of the object. So, memory is allocated at the time that we are creating an object from the class.
I want to add some attributes to the class, this can be facilitated by using constructures. Constructors are called automatically when an object is created from the class. I add name, phone, email and id as attributes of the class:

class employee:
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.email = email
        self.ids = ids

# pass initial setting params
rossi = employee('Rossi','34567322','rossi@gmail.com',1244)
miller = employee('Miller','44567991','miller@gmail.com',1534)

Here the constructor is accepting multiple input parameters for attributes values. I have created two objects from class, and each has its own properties.
Some notes:

  • self is the reference to the class itself and should be used always as the first parameter
  • name, phone, email and id are attributes and can have separate values for each object

By default, all of class members are public, which means they are accessible outside and inside of class:

print(f'First employee name and email: {rossi.name}, {rossi.email}, second employee name and email:{miller.name}, {miller.email}')

First employee name and email: Rossi, rossi@gmail.com, second employee name and email:Miller, miller@gmail.com

We can change the value of properties too:

rossi.email = 'rossi2@gmail.com'
print(rossi.email)

rossi2@gmail.com

Class attributes

There are two types of attributes:

  • Instance attribute: The value of the attribute is the same for each class's object
  • Class attribute: The value of the attribute is the same for all classes' objects

In the example below, name, phone, email, and ids are instance attributes. The description is the class attribute.

class employee:
    description = 'this is a description for an employee'
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.email = email
        self.ids = ids
        
rossi = employee('Rossi','34567322','rossi@gmail.com',1244)
miller = employee('Miller','44567991','miller@gmail.com',1534)

print(rossi.description,'***', rossi.email)

this is a description for an employee *** rossi@gmail.com

print(miller.description,'***', miller.email)

this is a description for an employee *** miller@gmail.com

As we can see the value of description is the same for both objects, while the value of email is different.

Let's try with an integer class attribute, here I have defined a counter variable which its value is going to be updated whenever an object is created from the class.

class employee:
    
    description = 'this is a description for an employee'
    counter = 0
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.email = email
        self.ids = ids
        employee.counter += 1

rossi = employee('Rossi','34567322','rossi@gmail.com',1244)
rossi.counter

1

miller = employee('Miller','44567991','miller@gmail.com',1534)
miller.counter

2

Consider that we have access to class attribute by class name followed by name of class attribute in the constructor.

Methods

Methods are used for doing an action on the attributes. In the example of employee class, I have created a method to print the specification of each employee:

class employee:
    description = 'this is a description for an employee'
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.email = email
        self.ids = ids
    def desc(self):
        print(f'Employee details are name: {self.name}, email:{self.email}, phone:{self.phone} & Id:{self.ids}')

rossi = employee('Rossi','34567322','rossi@gmail.com',1244)
miller = employee('Miller','44567991','miller@gmail.com',1243)

rossi.desc()

Employee details are name: Rossi, email:rossi@gmail.com, phone:34567322 & Id:1244

In this example, desc() is the method that is defined in the class's body and can have access to class properties such as name, phone, etc. Methods by default are public, so that is why we can call them outside of the class too.

Inheritance

Inheritance is when another class can derive the methods and properties of other class(es). It helps you to reuse functionalities of another class without rewriting everything. The inherited class is parent class and derived class is child class. Child class can override properties and methods of parent class (in most of the cases, more details are presented in the following).

In the previous example, an employee can have different forms such as programmer, manager, accountant, etc. I have defined a class for programmer which is going to get all of the properties of employee, the only change is that I override the class attribute: description.

class programmer(employee):
    description = 'this is a description for a programmer'
    pass

jackson = programmer('Jackson','42349327','jackson@gmail.com',1253)
jackson.description

'this is a description for a programmer'

The object of the child class has access to the parent's method:

jackson.desc()

Employee details are name: Jackson, email:jackson@gmail.com, phone:42349327 & Id:1253

Adding a new method

Child class can override parent's method or add new methods to its class body.
In the below example, we have programmer class which is the child of employee class. I have added a new method, teams(), to assign number of team members. For desc() method, first I called parent class method and then add extra information about team size. It is possible to access parent class by calling super() which is a reference to parent class.

class programmer(employee):
    description = 'this is a description for a programmer'

    def teams(self, num):
        self.teamSize = num
    def desc(self):
        super().desc()
        print(f'Team size is {self.teamSize}')
        
jackson = programmer('Jackson','42349327','jackson@gmail.com',1253)
jackson.teams(10)
jackson.desc()

Employee details are name: Jackson, email:jackson@gmail.com, phone:42349327 & Id:1253
Team size is 10

This is useful when we want to keep the main functionality of main method and add extra functionality to it.
Another way to access parent's class is by using parent class name:

class programmer(employee):
    description = 'this is a description for a programmer'

    def teams(self, num):
        self.teamSize = num
    def desc(self):
        employee.desc(self) #same as super().desc()
        print(f'Team size is {self.teamSize}')
        
jackson = programmer('Jackson','42349327','jackson@gmail.com',1253)
jackson.teams(10)
jackson.desc()

Employee details are name: Jackson, email:jackson@gmail.com, phone:42349327 & Id:1253
Team size is 10

Still, I can have direct access to parent class attributes:

print( jackson.email, jackson.name)

jackson@gmail.com Jackson

Override constructor

In this example, I want to add an extra attribute, title, to my programmer class and keep also the parent's attributes. This can be done by calling parent constructor and then add an extra attribute to its body:

class employee:
    description = 'this is a description for an employee'
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.email = email
        self.ids = ids
        print('This is employee class')
    def desc(self):
        print(f'Employee details are name: {self.name}, email:{self.email}, phone:{self.phone} & Id:{self.ids}')
        
class programmer(employee):
    description = 'this is a description for a programmer'

    def __init__(self,name,phone, email,ids):
        super().__init__(name,phone, email,ids) 
        self.title = 'programmer'
        print('This is programmer class')
        
    def teams(self, num):
        self.teamSize = num
    def desc(self):
        employee.desc(self) 
        print(f'Team size is {self.teamSize}')
    
jackson = programmer('Jackson','42349327','jackson@gmail.com',1253)

This is employee class
This is programmer class

By creating an object from child class, both constructors from parent & child are getting called, that's why we see the output from both constructors.
Still, I have access to properties from parent & child class:

print(f'Title is {jackson.title} and id is {jackson.ids}')

Title is programmer and id is 1253

Encapsulation

Encapsulation is another important feature in OOP, the main idea behind the concept is to hide and protect attributes and methods which should not be accessed directly. Consider that by default all class members are public and can be accessible outside and inside of the class.
To accomplish this, we need to know access modifiers which are:

  • Protected: In this way, the members of class are going to be accessible only by other members of the class and also their subclasses. For converting a member to be protected, you need to add _ (underscore) prefix to its name.
  • Private: Similar to protected class, with the difference that members of the class are going to be accessible just by members of the class and not in their subclasses. For converting a member to be private, you need to add __ (double underscore) prefix to its name. So, you may ask the question, that how you can access in subclasses to these members? This is possible to do by using getter & setter methods. We will see this in the next example.

Protected class example

Here I am defining an employee class with one protected member: email, which is specified by (_) prefix to its name.

class employee:
    description = 'this is a description for an employee'
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self._email = email
        self.ids = ids
        print('This is employee class')
    def desc(self):
        print(f'Employee details are name: {self.name}, email:{self._email}, phone:{self.phone} & Id:{self.ids}')
        
rossi = employee('Rossi','34567322','rossi@gmail.com',1244)
rossi.desc()

This is employee class
Employee details are name: Rossi, email:rossi@gmail.com, phone:34567322 & Id:1244

As you can see, we access protected members in desc() method.
You will get an error if you call the member outside of the class:

rossi.email

---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

 in 
----> 1 rossi.email

AttributeError: 'employee' object has no attribute 'email'

However, there is another way to access it:

rossi._email

'rossi@gmail.com'

What happened?! As you can see, the concept, is not totally working in python, and still, you can find a way to access protected members outside of class, and all of these are just a convention. But don't do it! It is not a good practice to change this functionality 🙂

Private class example

In this example, I have defined email as a private member, and two methods to get and modify email value. This is the best way to access private members:

class employee:
    description = 'this is a description for an employee'
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.__email = email
        self.ids = ids
        print('This is employee class')
    def desc(self):
        print(f'Employee details are name: {self.name}, email:{self.email}, phone:{self.phone} & Id:{self.ids}')
    def getEmail(self):
        print(f'Employee email is {self.__email}')
    def setEmail(self,email_new):
        self.__email = email_new
rossi = employee('Rossi','34567322','rossi@gmail.com',1244)

This is employee class

rossi.getEmail()

Employee email is rossi@gmail.com

rossi.setEmail('rossi143@gmail.com')
rossi.getEmail()

Employee email is rossi143@gmail.com

Polymorphism

Polymorphism means having multiple forms for an existed concept, it allows the same interface for different objects. So, in practice, you can have a function with the same name but different implementation. Consider the inheritance, the child class is inheriting methods and attributes of parent class. It is possible for child class to have a method with the same name as the parent but different implementation. Let's look at an example:

class employee:
    description = 'this is a description for an employee'
    def __init__(self,name,phone, email,ids):
        self.name = name
        self.phone = phone
        self.email = email
        self.ids = ids
        print('This is employee class')
    def desc(self):
        print(f'Employee details are name: {self.name}, & Id:{self.ids}')
        
class programmer(employee):
    def desc(self):
        print(f'Programmer details are email:{self.email} & phone:{self.phone}')

jackson = programmer('Jackson','42349327','jackson@gmail.com',1253)
jackson.desc()

This is employee class
Programmer details are email:jackson@gmail.com & phone:42349327

As you can see, this is similar to Overriding that we have seen before.

So, overriding is the same as polymorphism?

Almost! Polymorphism is a principle that you extend the base method functionality, which is achieved by overriding methods.

Data abstraction

Data abstraction means that the implementation of the application is hidden from user and we emphasize application usage. As an example, for using a microwave, we just know how to use it but don't know its details.
Why this is important? using data abstraction allows us to make applications more extensible and easier to share in complex systems and improve efficiencies. We can define both classes and methods as abstract. An abstract method doesn't have implementation and it is going to be defined in the subclasses.
Python by default doesn't provide abstract classes and it can be defined with ABC module which provides the base for defining Abstract Base classes(ABC).
Let's see all the concept through an example:

from abc import ABC, abstractmethod

class employee(ABC):
     @abstractmethod
     def desc(self):
         pass
        
class programmer(employee):
    def desc(self,size):
        print(f'Progammer group size is {size}')
        
class IT(employee):
    def desc(self,size):
        print(f'IT group size is {size}')
class business(employee):
    def desc(self,size):
        print(f'Business group size is {size}')
p = programmer()
p.desc(10)

b = business()
b.desc(40)

Progammer group size is 10
Business group size is 40

In the above example, employee is the abstract class with an abstract method of desc. There are two subclasses: programmer & IT which are derived from employee class. Each of these classes has been provided their own version of desc() implementation and customized it.
Note: Consider that we cannot initiate an instance from an abstract class (that's the reason we call them abstract!):

a= employee()

---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

 in 
----> 1 a= employee()

TypeError: Can't instantiate abstract class employee with abstract methods desc

That's the end of OOP concepts for Python.
the principles of OOP, is helping a lot in readability and efficiency for our codes, especially when the code is shared among multiple developers/teams. Compare to Java, python is not a fully object-oriented language, but it supports most of the functionalities.

In case you need more material to study:

Happy learning!

Author: Pari

Leave a Reply

Your email address will not be published. Required fields are marked *