Core Python: Part 5

Object-Oriented Programming

Core Python: Part 5

Check out the previous part: Core Python: Part 4

We have procedural and object-oriented as two categories in structured programming. The functions used in programs are procedural in nature that do not hold their own data or remember their state but are defined once and never modified. Object-oriented programming is an alternative to this that has an object representing a concept or a physical entity. An object will have its own data (attributes) and functions to manipulate the data.

  1. Inheritance

    A base class (or parent class) is a class from which other classes inherit properties and behaviors. It defines the common attributes and methods that are shared by its derived classes. A derived class (or subclass) is a class that inherits attributes and methods from a base class. An abstract class is a class that cannot be instantiated on its own and is meant to be subclassed by other classes. Abstract classes are useful for creating a consistent interface across multiple subclasses while enforcing certain behaviors.

     # Base class Animal
     class Animal:
         def speak(self):
             pass  # Placeholder for subclasses to implement
    
     # Derived class Dog that extends class Animal that overrides method speak
     class Dog(Animal):
         def speak(self):
             return "Woof!"
    
     # Derived class Dog that extends class Animal that overrides method speak
     class Cat(Animal):
         def speak(self):
             return "Meow!"
    
     # abstract class using the abc module
     from abc import ABC, abstractmethod
    
     class Shape(ABC):
         @abstractmethod
         def area(self):
             pass
    
  2. An instance of a class - object

     class Animal:
         def __init__(self, species):
             self.species = species
    
     # Derived class from Animal
     class Dog(Animal):
         def __init__(self, species, breed):
             # __init__ is called dunder method
             super().__init__(species)  # Calling parent class constructor
             self.breed = breed
    
     # Creating instances of the Dog class
     dog = Dog("Canine", "Labrador")
     print(dog.species)  # Output: Canine
     print(dog.breed)    # Output: Labrador
    
  3. Encapsulation

    The primary goal of encapsulation is to control access to the internal state of an object, ensuring that the data is not directly manipulated from outside the object. The __age attribute is encapsulated using the double underscore (__) prefix. Direct access to it from outside the class is prevented. Instead, getter and setter methods (get_age() and set_age()) are provided to access and modify the attribute, respectively.

     class Student:
         def __init__(self, name, age):
             self.name = name
             self.__age = age  # Encapsulated attribute
    
         def get_age(self):
             return self.__age
    
         def set_age(self, age):
             if age > 0:
                 self.__age = age
    
     student = Student("Alice", 20)
     print(student.name)        # Accessing public attribute directly
     print(student.get_age())  # Accessing encapsulated attribute using method
    
     student.set_age(22)       # Modifying encapsulated attribute using method
     print(student.get_age())  # Output: 22
    
  4. Polymorphism

    It enables methods to be implemented in different ways in various subclasses while adhering to a common interface. Hence objects of different classes behave as objects of a common base class.

    Functions that work with several types are called polymorphic. Polymorphism can facilitate code reuse. For example, the built-in function sum, which adds the elements of a sequence, works as long as the elements of the sequence support addition

     class Shape:
         def area(self):
             pass
    
     class Circle(Shape):
         def __init__(self, radius):
             self.radius = radius
    
         def area(self):
             return 3.14 * self.radius * self.radius
    
     class Rectangle(Shape):
         def __init__(self, length, width):
             self.length = length
             self.width = width
    
         def area(self):
             return self.length * self.width
    
     def print_area(shape):
         print("Area:", shape.area())
    
     circle = Circle(5)
     rectangle = Rectangle(4, 6)
     print_area(circle)     # Output: Area: 78.5
     print_area(rectangle)  # Output: Area: 24
    
  5. Abstraction

    Only Shape will be visible for users who can see that area method exists but cannot see the details of the area method.

     from abc import ABC, abstractmethod
    
     class Shape(ABC):
         @abstractmethod
         def area(self):
             pass
    
     class Circle(Shape):
         def __init__(self, radius):
             self.radius = radius
    
         def area(self):
             return 3.14 * self.radius * self.radius
    
     class Rectangle(Shape):
         def __init__(self, length, width):
             self.length = length
             self.width = width
    
         def area(self):
             return self.length * self.width
    
     circle = Circle(5)
     rectangle = Rectangle(4, 6)
     print(circle.area())     # Output: 78.5
     print(rectangle.area())  # Output: 24
    
  6. Overloading

     class MathOperations:
         def add(self, a, b=None):
             if b is None:
                 return a
             return a + b
    
     math_ops = MathOperations()
     print(math_ops.add(5))      # Output: 5
     print(math_ops.add(2, 3))   # Output: 5
    
     2 * 2 = 4
     '2' * 2 = '22' # * behaves differentely when datatypes are different
    
  7. __str__() and __repr__()

    They are special methods that can be defined in a class to control how instances of that class are represented as strings.

     class Point:
         def __init__(self, x, y):
             self.x = x
             self.y = y
    
         def __repr__(self):
             return f"Point({self.x}, {self.y})"
    
     p = Point(3, 5)
     print(repr(p))  # Output: Point(3, 5)
     print(str(p))  # Output: (3, 5)
    

Check out the next part: Core Python: Part 6