Objects and Object Oriented Programming in Python
Tags: python, programming
Published: 2023-02-16 06:40:18

In Python, as well as in many other programming languages, object-oriented programming (OOP) is a popular programming paradigm. 

OOP is a way of structuring code that allows us to represent real-world objects as software objects.

### Classes and Objects:

An object is an instance of a class. 

A class is a blueprint for creating objects, which defines the attributes and methods that the objects will have. 

An attribute is a piece of data that an object has, while a method is a function that operates on the object's data.

Consider the below example,

```
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def say_hello(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")
```
        
In this example, we define a class called Person, which has two attributes, name and age, and one method, say_hello.

When a new instance of the class is created, a unique method named __init__ is invoked.

The self parameter refers to the instance of the class that is being created, and it is used to set the values of the name and age attributes.

Consider the below example,

```
person1 = Person("Alice", 25)
In this example, we create a new object called person1 of the Person class, with the name "Alice" and age 25.
```

We can then call the say_hello method on the person1 object:


```
person1.say_hello()
```

This will output "Hello, my name is Alice and I am 25 years old."


OOP allows us to write more modular, reusable, and maintainable code. 

We can define a class once and then create many instances of it, each with their own unique data. We can also create subclasses that inherit attributes and methods from a parent class, and add their own unique attributes and methods.


### Polymorphism:

An object's capacity to assume different forms is known as polymorphism.

In Python, polymorphism is achieved through the use of method overriding and method overloading.

#### Method Overriding:

Method overriding is the ability of a subclass to override a method of its superclass. 

Here is an example,

```
class Animal:
    def sound(self):
        print("Animal makes a sound")
        
class Dog(Animal):
    def sound(self):
        print("Dog barks")
        
class Cat(Animal):
    def sound(self):
        print("Cat meows")
```
        
In this example, we define a class called Animal, which has a method called sound.

We then define two subclasses, Dog and Cat, which override the sound method to provide their own implementation.

#### Method Overloading:

Method overloading is the ability to define multiple methods with the same name, but with different parameters. 

In Python, method overloading is achieved through the use of default arguments. 

Here is an example,

```
class Math:
    def add(self, a, b, c=0):
        return a + b + c
    
m = Math()
print(m.add(1, 2)) # Output: 3
print(m.add(1, 2, 3)) # Output: 6
```

In this example, we define a class called Math, which has a method called add. 

The add method takes two required parameters, a and b, and an optional parameter, c. 

If the c parameter is not provided, it defaults to 0. 

This allows us to call the add method with either two or three arguments.


### Encapsulation:

Encapsulation is the concept of hiding the internal details of an object from the outside world, and only exposing a public interface. 

This allows us to change the internal implementation of an object without affecting the rest of the program. 

Encapsulation is accomplished in Python by using private properties and functions.

Private attributes and methods are indicated by a double underscore before their name. 

Here is an example,


```
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance
        
    def deposit(self, amount):
        self.__balance += amount
        
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")
            
    def get_balance(self):
        return self.__balance
```
        
In this example, we define a class called BankAccount, which has a private attribute __balance. 

The deposit and withdraw methods allow us to modify the __balance attribute, while the get_balance method allows us to access the __balance attribute. 

By making the __balance attribute private, we prevent the outside world from directly modifying it.

### Inheritance:

Inheritance is the concept of creating a new class that is a modified version of an existing class.

The term "superclass" refers to the current class, and "subclass" to the new class. 

The subclass inherits all of the attributes and methods of the superclass, and can also have its own unique attributes and methods.

Example:

```
class parent_class:
    x = "Parent class variable"
        
class child_class(parent_class):
    pass

c1 = Test()
obj = child_class()
print(obj.x)
```

Output:

Parent class variable

Here we can observe that we have created the object of class child_class. 

And we do not have any class members in our class child_class. 

But we inherited the properties of class parent_class into class child_class. 

The class variable in class parent_class is inherited from class child_class. 

Hence it is called using the object of class child_class.


Python supports different types of inheritance, which are:

1. Single inheritance
2. Multiple inheritances
3. Multilevel inheritance
4. Hierarchical inheritance
5. Hybrid inheritance

#### 1. Single inheritance:
 
This is the simplest type of inheritance, where a child class inherits from a single parent class. 

For example:

```
class ParentClass:
    def parent_method(self):
        print("This is a parent method.")

class ChildClass(ParentClass):
    def child_method(self):
        print("This is a child method.")

child = ChildClass()
child.parent_method()   
child.child_method()  
```
  
#### 2. Multiple inheritances: 

This type of inheritance allows a child class to inherit from multiple parent classes. 

In Python, this is achieved by listing the parent classes in a comma-separated list in the parentheses. 

For example:

```
class ParentClass1:
    def parent1_method(self):
        print("This is a parent 1 method.")

class ParentClass2:
    def parent2_method(self):
        print("This is a parent 2 method.")

class ChildClass(ParentClass1, ParentClass2):
    def child_method(self):
        print("This is a child method.")

child = ChildClass()
child.parent1_method()   
child.parent2_method()  
child.child_method()    
```
 

#### 3. Multi-level inheritance: 

This type of inheritance involves creating a hierarchy of classes, where a child class inherits from a parent class, which in turn, inherits from another parent class. 

For example:

```
class GrandParentClass:
    def grandparent_method(self):
        print("This is a grandparent method.")

class ParentClass(GrandParentClass):
    def parent_method(self):
        print("This is a parent method.")

class ChildClass(ParentClass):
    def child_method(self):
        print("This is a child method.")

child = ChildClass()
child.grandparent_method()  # This is a grandparent method.
child.parent_method()       # This is a parent method.
child.child_method()        # This is a child method.

```

#### 4. Hierarchical inheritance:

when more than one child class is derived from or inherited from a single (same) parent class. 

Then this type of inheritance is called hierarchical inheritance.


```
class ParentClass:
    def parent_method(self):
        print("This is a parent method.")

class ChildClass1(ParentClass):
    def child1_method(self):
        print("This is a child 1 method.")

class ChildClass2(ParentClass):
    def child2_method(self):
        print("This is a child 2 method.")

child1 = ChildClass1()
child1.parent_method()   # This is a parent method.
child1.child1_method()   # This is a child1 method.

child2 = ChildClass2()
child2.parent_method()   # This is a parent method.
child2.child2_method()   # This is a child2 method.
```

#### 5. Hybrid inheritance: 

This type of inheritance combines multiple types of inheritance, such as single, multiple, and multi-level inheritance, to create a more complex inheritance hierarchy.


```
class GrandParentClass:
    def grandparent_method(self):
        print("This is a grandparent method.")

class ParentClass1(GrandParentClass):
    def parent1_method(self):
        print("This is a parent 1 method.")

class ParentClass2:
    def parent2_method(self):
        print("This is a parent 2 method.")

class ChildClass(ParentClass1, ParentClass2):
    def child_method(self):
        print("This is a child method.")

child = ChildClass()
child.grandparent_method()  
child.parent1_method()     
child.parent2_method()      
child.child_method()        
```

In the above example, ChildClass is inheriting from two parent classes, ParentClass1 and ParentClass2.

ParentClass1 itself inherits from GrandParentClass, creating a multi-level inheritance hierarchy.

Hybrid inheritance can become complex and difficult to understand, so it's important to use it judiciously and keep the hierarchy as simple and intuitive as possible.

### Data Abstraction:

Data abstraction is a technique used in object-oriented programming to hide the complexity of the data and expose only the necessary information to the user. 

It provides a simplified view of the data, making it easier to understand and work with. 

In Python, data abstraction can be achieved using abstract classes and interfaces.

Here's an example of data abstraction using abstract classes:


```
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

class Rectangle(Shape):
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * (self.length + self.width)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14 * self.radius

rect = Rectangle(5, 10)
print(rect.area())       # 50
print(rect.perimeter())  # 30

circ = Circle(7)
print(circ.area())       # 153.86
print(circ.perimeter())  # 43.96
```

In this example, Shape is an abstract class that defines the interface for calculating the area and perimeter of a shape.

Rectangle and Circle are concrete classes that implement the Shape interface. 

By using abstract classes, we can create a simplified view of the shapes that hides the complexity of the calculations.

Another way to achieve data abstraction in Python is using interfaces, which are a collection of abstract methods that define a contract between a class and the outside world. 

The abc module provides the ABC class, which can be used to define interfaces:


```
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

dog = Dog()
print(dog.make_sound())  # Woof!

cat = Cat()
print(cat.make_sound())  # Meow!
```

In this example, Animal is an interface that defines the make_sound method.

Dog and Cat are concrete classes that implement the Animal interface by providing their own implementation of the make_sound method. 

By using interfaces, we can create a simplified view of the animals that hides the complexity of their behavior.

In conclusion, if you want to write more modular, reusable, and maintainable code, object-oriented programming (OOP) in Python is a great choice. With OOP, you can represent real-world objects as software objects and write code that is easier to maintain, debug, and extend. By defining classes that encapsulate attributes and methods, you can create many instances of objects with unique data, and even create subclasses that inherit attributes and methods from a parent class. With these powerful concepts, you can build complex systems and solve real-world problems with ease. Whether you are a beginner or an experienced developer, mastering OOP in Python is a skill that will undoubtedly take your programming abilities to the next level.

### References:

- GeeksforGeeks - "Python OOPs Concepts." https://www.geeksforgeeks.org/python-oops-concepts/