OOP with Python - Part 2

OOP with Python - Part 2

OOP with Python - Part 2

We covered the basics of Object-Oriented Programming (OOP), including Classes and Objects, Encapsulation, and Inheritance in OOP with Python - Part 1. In this second part, we will discuss the remaining two key principles of OOP; Abstraction and Polymorphism.

Abstraction

Abstraction is the concept of hiding the complex implementation details of a system and exposing essential features of an object. This allows us to focus on what an object does but not how it is done. This can reduce programming complexity and enhance code maintainability.

In Python, abstraction can be achieved using abstract classes and methods provided by the abc module. An abstract class is a class that cannot be instantiated and typically contains one or more abstract methods. An abstract method is a method that is declared but contains no implementation. Subclasses of the abstract class must provide implementations for all abstract methods. Hence, abstract class acts as a blueprint for subclasses.

Let’s extend our Shape example from previous article to include abstraction. In real life, since Shape is more of an umbrella term which encompasses different types of shapes, we may implement it as an abstract class. Then, we can implement concrete subclasses like Square or Rectangle to represent different types of shapes.

from abc import ABC, abstractmethod


class Shape(ABC):  # Abstract class Shape

    shape_type = "Quadrilateral"  # Class attribute

    def __init__(self, name):  # Constructor
        self.shape_name = name  # Public object attribute

    @abstractmethod
    def set_area(self):  # Abstract setter method
        pass

    @abstractmethod
    def get_area(self):  # Abstract getter method
        pass

Here, we have defined an abstract class Shape with two abstract methods: set_area and get_area. Inside the methods, we may use the pass statement which acts as a placeholder code snippet in Python. This class cannot be instantiated directly. Instead, we will create concrete subclasses that provide implementations for the abstract methods.

Let’s implement the Square subclass.

class Square(Shape):
    """
    Subclass Square 
    Extends abstract class Shape
    """

    def __init__(self, name, side): # Constructor
        super().__init__(name)
        self.side = side    # Public object attribute
        self.__area = 0     # Private object attribute

    def set_area(self):     # Concrete setter method
        self.__area = self.side**2

    def get_area(self):     # Concrete getter method
        return self.__area

In the subclass, we have provided concrete implementations for the abstract methods. Now, we can create objects of the Squareclass.

square = Square("Square", 8)
square.set_area()

print(f"Shape Name: {square.shape_name}, Shape Type: {Shape.shape_type}")
print(f"Area: {square.get_area()}")

The output of this code will be:

Shape Name: Square, Shape Type: Quadrilateral
Area: 64


Polymorphism

In OOP, polymorphism is the concept of treating objects of different subclasses as objects of a common superclass. Polymorphism is achieved through method overriding and method overloading. However, Python directly doesn’t support method overloading (compile-time polymorphism), hence, we will discuss method overriding in this chapter.

Method overloading is having multiple methods in the same class with same name but different parameters (type or value or both). In Python, we can achieve similar functionality by using default arguments or variable-length arguments.

Method overriding (runtime polymorphism) occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. The overridden method in the subclass must have the same name and the parameter list as the method in the superclass.

To illustrate polymorphism, we will implement another sub class, Rectangle. In the following example we have implemented the set_area method to calculate the area of a rectangle. This implementation replaces the pass statement of the set_area method of the super class Shape.

class Rectangle(Shape):
    """
    Subclass Rectangle 
    Extends abstract class Shape
    """

    def __init__(self, name, length, width):    # Constructor
        super().__init__(name)
        self.length = length    # Public object attribute
        self.width = width      # Public object attribute
        self.__area = 0     # Private object attribute

    def set_area(self):     # Concrete setter method
        self.__area = self.length * self.width

    def get_area(self):     # Concrete getter method
        return self.__area

Now, we can create an object of the Rectangle subclass.

rectangle = Rectangle("Rectangle", 10, 8)
rectangle.set_area()

print(f"Shape Name: {rectangle.shape_name}, Shape Type: {Shape.shape_type}")
print(f"Area: {rectangle.get_area()}")

The output of this code will be:

Shape Name: Rectangle, Shape Type: Quadrilateral
Area: 80


Complete OOP Code Example

Let’s put everything together and write a complete example to cover all the four concepts we discussed.

from abc import ABC, abstractmethod


class Shape(ABC):  # Abstract class Shape

    shape_type = "Quadrilateral"  # Class attribute

    def __init__(self, name):  # Constructor
        self.shape_name = name  # Public object attribute

    @abstractmethod
    def set_area(self):  # Abstract setter method
        pass

    @abstractmethod
    def get_area(self):  # Abstract getter method
        pass


class Square(Shape):
    """
    Subclass Square
    Extends abstract class Shape
    """

    def __init__(self, name, side):  # Constructor
        super().__init__(name)
        self.side = side  # Public object attribute
        self.__area = 0  # Private object attribute

    def set_area(self):  # Concrete setter method
        self.__area = self.side**2

    def get_area(self):  # Concrete getter method
        return self.__area


class Rectangle(Shape):
    """
    Subclass Rectangle
    Extends abstract class Shape
    """

    def __init__(self, name, length, width):  # Constructor
        super().__init__(name)
        self.length = length  # Public object attribute
        self.width = width  # Public object attribute
        self.__area = 0  # Private object attribute

    def set_area(self):  # Concrete setter method
        self.__area = self.length * self.width

    def get_area(self):  # Concrete getter method
        return self.__area


square = Square("Square", 8)
square.set_area()
rectangle = Rectangle("Rectangle", 10, 8)
rectangle.set_area()

print(f"Shape Name: {square.shape_name}, Shape Type: {Shape.shape_type}")
print(f"Area: {square.get_area()}")
print(f"Shape Name: {rectangle.shape_name}, Shape Type: {Shape.shape_type}")
print(f"Area: {rectangle.get_area()}")

Output:

Shape Name: Square, Shape Type: Quadrilateral
Area: 64
Shape Name: Rectangle, Shape Type: Quadrilateral
Area: 80


Our code has implemented encapsulation through separate classes with setters and getters, ensuring data integrity. Each class contains data and methods related to a specific shape. To implement inheritance we have extended Square and Rectangle classes to the Shape class, providing a structure for code reuse. Abstraction is implemented by defining the Shape class as an abstract class, reducing complexity. Polymorphism is shown by overriding the set_area method in Square and Rectangle classes, enabling flexibility.


Comments

Popular posts from this blog

Data Structures - Part 1

OOP with Python - Part 1