Understanding of Python OOPs Capabilities

Object-Oriented Programming (OOPS) is a powerful paradigm that allows developers to model real-world entities within their programs, making code more modular, reusable, and easier to maintain. Python, known for its versatility and simplicity, supports OOPS principles seamlessly. In this post, we will explore key concepts of OOPS in Python, such as inheritance, polymorphism, encapsulation, and abstraction.

Inheritance:

Inheritance allows a class to inherit attributes and methods from a parent class. This enables code reuse and helps in creating a clear class hierarchy. There are different types of inheritance in Python: single-level, multi-level, and multiple inheritance. Let’s explore each with examples.

  • Single-level Inheritance: It enables a class to derive from a single parent class. This creates a straightforward relationship between the two classes, helping to keep the code modular and reusable.
#Input Screen

class Animal:
     def speak(self):
         print("Animal speaks")
 
class Dog(Animal):
     def bark(self):
         print("Dog barks")
 
# Creating an instance of the Dog class
 my_dog = Dog()
 my_dog.speak()  # Inherits speak() from Animal class
 my_dog.bark()   # Uses its own method bark()
#Output Screen

Animal speaks
Dog barks

This code demonstrates single-level inheritance in Python, where the class Dog() inherits for the class Animal. The ‘Animal’ class has a method ‘speak()’, and the ‘Dog’ class, which extends ‘Animal’, inherits the ‘speak()’ method and adds its own method ‘bark()’.

In the example:

  • my_dog.speak() calls the speak() method from the Animal class.
  • my_dog.bark() calls the bark() method, which is unique to the Dog class.

Single-level inheritance allows the ‘Dog’ class to reuse the functionality of the ‘Animal’ class while adding its own behavior.

  • Multi-level Inheritance: Multi-level inheritance extends the concept by introducing a chain of inheritance, where each child class inherits properties and behaviours from its parent class, and so on. This enables the reuse of code across multiple levels of hierarchy.
#Input Screen

# Base class (Grandparent)
class Animal:
    def speak(self):
        return "Animal speaks"
 
# Derived class (Parent)
class Mammal(Animal):
    def walk(self):
        return "Mammal walks"
 
# Further derived class (Child)
class Dog(Mammal):
    def bark(self):
        return "Dog barks"
 
# Creating an object of the most derived class
dog = Dog()
 
print(dog.speak())  # Inherited from Animal
print(dog.walk())   # Inherited from Mammal
print(dog.bark())   # Defined in Dog

#Output Screen

Animal speaks
Mammal walks
Dog barks

This code example illustrates multi-level inheritance in Python, where a class is derived from another derived class, forming a chain of inheritance.

  • The base class ‘Animal’ defines the ‘speak()’ method.
  • The ‘Mammal’ class inherits from ‘Animal’ and adds a new method, ‘walk()’.
  • The ‘Dog’ class, derived from ‘Mammal’, defines its own method, ‘bark()’.

In this structure:

  • ‘dog.speak()’ calls the ‘speak()’ method from the ‘Animal’ class.
  • ‘dog.walk()’ calls the ‘walk()’ method from the ‘Mammal’ class.
  • ‘dog.bark()’ calls the ‘bark()’ method from the ‘Dog’ class.

This shows how each class builds upon the functionality of its parent class, adding more specific behaviors as you move down the hierarchy.

  • Multiple Inheritance: Multiple inheritance allows a subclass to inherit from more than one parent class. This can be a powerful tool but requires careful handling to avoid ambiguity, particularly when multiple parents implement methods with the same name.
#Input Screen

class Flyer:
     def fly(self):
         print("Can fly")
 
class Swimmer:
     def swim(self):
         print("Can swim")
 
class Amphibian(Flyer, Swimmer):
     pass
 
my_amphibian = Amphibian()
 my_amphibian.fly()   # Inherits fly() from Flyer class
 my_amphibian.swim()  # Inherits swim() from Swimmer class
#Output Screen

Can fly
Can swim

This example demonstrates multiple inheritance in Python, where a class can inherit from more than one parent class.

  • The ‘Flyer’ class defines the ‘fly()’ method.
  • The ‘Swimmer’ class defines the ‘swim()’ method.
  • The ‘Amphibian’ class inherits from both ‘Flyer’ and ‘Swimmer’ but doesn’t define any new methods.

When you create an instance of ‘Amphibian’:

  • ‘my_amphibian.fly()’ calls the ‘fly()’ method from the ‘Flyer’ class.
  • ‘my_amphibian.swim()’ calls the ‘swim()’ method from the ‘Swimmer’ class.

This allows the ‘Amphibian’ class to combine behaviors from multiple parent classes.

Polymorphism

Polymorphism in Python refers to the ability of objects to take on multiple forms. It is achieved through duck typing and dynamic typing, allowing objects to be used based on their behaviour rather than their explicit type. This enhances flexibility and code reusability.

  • Method Overriding: Method overriding in Python is a feature that allows a subclass (child class) to provide a specific implementation of a method that is already defined in its superclass (parent class). When the method in the child class has the same name, same parameters, and same return type as the method in the parent class, the child class’s method overrides the parent class’s method.
#Input Screen

class Animal:
    def sound(self):
        return "Some sound"
 
class Dog(Animal):
    def sound(self):
        return "Bark"
 
class Cat(Animal):
    def sound(self):
        return "Meow"
 
# Create instances
dog = Dog()
cat = Cat()
 
# Call the overridden method
print(dog.sound())  # Output: Bark
print(cat.sound())  # Output: Meow
#Output Screen

Bark
Meow

This example demonstrates method overriding in Python, where a subclass provides its own implementation of a method that is already defined in its parent class.

  • The ‘Animal’ class defines a ‘sound()’ method that returns ‘”Some sound”‘.
  • Both ‘Dog’ and ‘Cat’ classes inherit from ‘Animal’ and override the ‘sound()’ method with their own specific behaviors:
    1. The ‘Dog’ class overrides it to return ‘”Bark”‘.
    2. The ‘Cat’ class overrides it to return ‘”Meow”‘.

When you create instances of ‘Dog’ and ‘Cat’ and call the ‘sound()’ method:

  • ‘dog.sound()’ returns ‘”Bark”‘.
  • ‘cat.sound()’ returns ‘”Meow”‘.

This demonstrates how subclasses can modify or extend the behavior of inherited methods.

  • Operator Overloading: Operator Overloading is a fundamental concept in object-oriented programming that allows you to redefine the behavior of built-in operators (like +, -, *, /, ==, <, >, etc.) for your custom objects. This enables you to create more intuitive and expressive code by making it easier to work with your objects familiarly.
#Input Screen

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
    def __add__(self, other_point):
        return Point(self.x + other_point.x, self.y + other_point.y)
 
    def __str__(self):
        return f"Point({self.x}, {self.y})"
 
p1 = Point(1, 2)
p2 = Point(3, 4)
result = p1 + p2  # Calls __add__ method for the '+' operator
print(result)     # Prints: Point(4, 6)
#Output Screen

-11 + 23i

This example demonstrates operator overloading in Python, where you redefine how operators like ‘*’ behave for custom objects.

Here, the ‘ComplexNumber’ class is created to represent complex numbers, with two attributes: ‘real’ and ‘imag’ (for real and imaginary parts).

  • The ‘__mul__’ method is overridden to define how the multiplication (‘*’) operator works between two ‘ComplexNumber’ objects.
  • It calculates the real and imaginary parts of the product based on the formula for multiplying complex numbers and returns a new ‘ComplexNumber’.
  • The ‘__str__’ method is overridden to provide a human-readable string representation of the complex number.

When ‘c1 * c2’ is executed, Python calls the ‘__mul__’ method to perform the multiplication, and the result is printed as ‘-11 + 23i’.

Encapsulation:

Encapsulation is one of the most important principles of OOPS. It essentially combines the data (attributes) with the methods (functions) operating on it into a single unit, which is called a class. This practice restricts nuanced access to some of an object’s components and avoids accidental changes to data. In Python encapsulation is enabled through defining access modifiers: public, protected, and private.

class Employee:
    def __init__(self, name, salary):
        self.name = name          # Public attribute
        self._salary = salary     # Protected attribute
        self.__bonus = 500        # Private attribute

    def display(self):
        print(f"Name: {self.name}")
        print(f"Salary: {self._salary}")
        print(f"Bonus: {self.__bonus}")  # Accessing private attribute within the class

# Creating an object of Employee class
emp = Employee("John", 50000)

# Accessing public attribute
print(emp.name)  # Output: John

# Accessing protected attribute
print(emp._salary)  # Output: 50000 (Note: Can be accessed but should be treated as protected)

# Trying to access private attribute directly (will raise an error)
# print(emp.__bonus)  # AttributeError: 'Employee' object has no attribute '__bonus'

# Accessing private attribute using name mangling
print(emp._Employee__bonus)  # Output: 500

In the example:

  •  name is a public attribute and is accessible from outside the class.
  • _salary (includes underscore) is a protected attribute can be accessed within the class but can also be accessed in the derived classes.
  • __bonus (includes double underscore) is a private attribute and is accessible only within the class. Nonetheless, you can still access and change it from outside the class through name mangling (_ClassName__attribute).

Data Abstraction:

Data abstraction is a core concept underlying Python’s object-oriented programming approach, which aids the user in interaction by concealing the hidden complexity of the implementations and providing only the relevant information to that user. When driving a car, for instance, one would apply the accelerator and brake without bothering about how the engine truly works inside. Similarly, data abstraction allows programmers to achieve well-organized and clean code by hiding such intricate details that are never necessary for users, exposing only the functions and data that are needed. In Python, this is done with the help of the ABC module, short for Abstract Base Class, which uses the @abstractmethod decorator to define methods that must be implemented by any subclass. Data abstraction focuses on the irrelevant details for the user and thus helps organize code, enhance maintenance, and promote collaboration; hence, developers would find it easy to deal with the same code, while users, in turn, would understand the program without much hassle. It promotes code reusability—a very good achievement—enhancing clarity and productivity altogether.

#Input Screen

from abc import ABC, abstractmethod

# Abstract class
class Animal(ABC):
    # Abstract method
    @abstractmethod
    def sound(self):
        pass
# Concrete class implementing the abstract class
class Dog(Animal):
    def sound(self):
        return "Bark"

class Cat(Animal):
    def sound(self):
        return "Meow"

# Creating objects of the concrete classes
dog = Dog()
cat = Cat()

print(dog.sound())  # Output: Bark
print(cat.sound())  # Output: Meow
#Output Screen

Bark 
Meow

This example illustrates data abstraction in Python using abstract class and abstract methods.

  • The ‘Animal’ class is an abstract class defined using the ‘ABC’ module, which stands for Abstract Base Class.
  • The ‘sound’ method in the ‘Animal’ class is an abstract method, declared using @ abstract method decorator. It does not provide any implementation and must therefore be overridden by subclasses.
  • Both the ‘Dog’ and ‘Cat’ are concrete classes that derive their behaviors from ‘Animal’ and implement their own version of the ‘sound’ method.

When you create instances of ‘Dog’ and ‘Cat’:

  •   ‘dog’ would return ‘”Bark”‘ for ‘sound()’.
  •   ‘cat’ would return ‘”Meow”‘ for ‘sound()’.

This demonstrates how abstraction allows a general structure to be provided in the abstract class while leaving particulars to the concrete subclasses.

Conclusion

In conclusion, our exploration of Object-Oriented Programming (OOPS) in Python has provided valuable insights into fundamental concepts such as inheritance, polymorphism, encapsulation, and data abstraction. These principles offer developers a robust toolkit for crafting efficient, maintainable, and scalable code. By harnessing these OOPS concepts judiciously, developers can establish a solid foundation for building resilient and adaptable software solutions.

By – Nikhil Bhatia

Leave a Comment

Your email address will not be published.