• Python is an object oriented programming language which contains data (attributes) and functions (methods). Python allows for the creation of reusable and modular code by modeling real-world entities.
Below are the key concepts of OOPs :
  1. Class: A blueprint for creating objects. It defines the attributes and methods that the objects will have.
  2. Object: An instance of a class. Objects hold data and interact with each other through methods.
  3. Encapsulation: Restricting access to certain details of an object and exposing only essential features.
  4. Inheritance: Allowing a class to inherit properties and methods from another class, enabling reusability.
  5. Polymorphism: Allowing different classes to define methods with the same name but different behavior.
  6. Abstraction: Hiding the implementation details and showing only the functionality to the user.

Let’s take deep dive in each concept one by one.

1. Class
  • It defines the attributes (data) and methods (functions) that the objects created from the class will have. Classes are the foundation of Object-Oriented Programming (OOP) in Python, enabling the creation of reusable and modular code.
  • To define a class, use the class keyword:
  • a class is a user defined data type, where we give a name to the data type and This data type may contain variables inside of it with where each variable can having different data types.

  • Class can have multiple Attributes and Methods inside of it.

        1.1 Attributes : are a Variables that belong to the class or an instance of the class. If we modify a class variable through an instance, a new instance variable is getting created. This separates the modified value from the original class variable, which remains unchanged for other instances.

      • Instance Attributes: Defined inside the constructor (__init__) and belong to each object.
      • Class Attributes: Shared across all instances of the class.
class Example:
    shared_attr = "I am class attribute."    # Class attribute
    def __init__(self, value):
        self.instance_attr = value           # Instance attribute

       1.2 Methods : are a Functions defined within the class that describe the behaviour of the objects. below are the different types of methods :

      • Instance Methods: Operate on instance attributes and use self as the first parameter. When you add a method to a class, we must have self as the first argument of that function. but when you call a function you don’t actually input self
      • Class Methods: Operate on class attributes and use @classmethod and cls.
      • Static Methods: Don’t operate on instance or class attributes and use @staticmethod
  •  
class Employee:
    def __init__(self, name, age):               # Instance method
        self.name = name                         # Instance attribute
        self.age = age
    
    @classmethod
    def class_method(cls):                       #Class method
        print("This is a class method.")

    @staticmethod
    def static_method():                         # Static method
        print("This is a static method.")

2. Object

To create an object, call the class as if it were a function. After creating a new object we can modify the object properties, also we can object properties or object.

e1 = Employee("Martha", 18)          #Create Instance of Employee class
e1.age = 40                          #Modify Properties
del e1.age                           #Delete object property
del e1                               #Delete object
3. Encapsulation

It refers to restricting access to certain details of an object and exposing only the necessary parts. Encapsulation allows data (attributes) and methods (functions) to be bundled together into a single unit (class) while controlling access to them.

class BankAccount:
    def __init__(self, account_number, balance):
        self.__account_number = account_number              # Private attribute
        self.__balance = balance                            # Private attribute
        
    def deposit(self, amount):                              # Public method to deposit money
        if amount > 0:
            self.__balance += amount
            print(f"Deposited: ${amount}")
        else:
            print("Deposit amount must be positive.")
            
    def withdraw(self, amount):                             # Public method to withdraw money
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"Withdrawn: ${amount}")
        else:
            print("Insufficient balance or invalid amount.")

    def get_balance(self):                                  # Getter method to retrieve the balance
        return self.__balance
        
    def set_balance(self, new_balance):                     # Setter method to modify the balance (with validation)
        if new_balance >= 0:
            self.__balance = new_balance
        else:
            print("Balance cannot be negative.")

account = BankAccount("1234567890", 500)                    # Create an object of the BankAccount class

# Access private attributes using public methods
account.deposit(200)                                        # Deposited: $200
account.withdraw(100)                                       # Withdrawn: $100
print(account.get_balance())                                # 600

# Attempting to directly access private attributes (will fail)
# print(account.__balance)                                  # AttributeError

4. Inheritance

It allows a class (called the child class or subclass) to inherit attributes and methods from another class (called the parent class or base class). This promotes code reuse and establishes a hierarchical relationship between classes.

              Type of inheritance :
    1. Single Inheritance: A child class inherits from one parent class.
    2. Multiple Inheritance: A child class inherits from multiple parent classes.
    3. Multilevel Inheritance: A child class inherits from a parent class, which itself inherits from another parent class.
    4. Hierarchical Inheritance: Multiple child classes inherit from the same parent class.
    5. Hybrid Inheritance: A combination of two or more types of inheritance.
##### Single Inheritance Example #####
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} makes a sound.")

class Dog(Animal):
    def speak(self):
        print(f"{self.name} barks.")

dog = Dog("Buddy")                                  # Create an object of the child class
dog.speak()  # Output: Buddy barks

##### Multiple Inheritance Example #####
class Flyable:
    def fly(self):
        print("Can fly.")

class Swimable:
    def swim(self):
        print("Can swim.")

class Duck(Flyable, Swimable):
    def quack(self):
        print("Quacks.")

# Create an object of the Duck class
duck = Duck()
duck.fly()                                              # Output: Can fly.
duck.swim()                                             # Output: Can swim.
duck.quack()                                            # Output: Quacks.

##### Multilevel Inheritance Example #####
class Vehicle:
    def move(self):
        print("Vehicle is moving.")

class Car(Vehicle):
    def drive(self):
        print("Car is driving.")

class SportsCar(Car):
    def accelerate(self):
        print("Sports car accelerates quickly.")

# Create an object of the SportsCar class
sports_car = SportsCar()
sports_car.move()                                   # Output: Vehicle is moving.
sports_car.drive()                                  # Output: Car is driving.
sports_car.accelerate()                             # Output: Sports car accelerates quickly.

##### Hierarchical Inheritance Example #####
# Base class
class Parent:
	def func1(self):
		print("This function is in parent class.")

# Derived class1
class Child1(Parent):
	def func2(self):
		print("This function is in child 1.")

# Derivied class2
class Child2(Parent):
	def func3(self):
		print("This function is in child 2.")

# Driver's code
object1 = Child1()
object2 = Child2()
object1.func1()                                     #This function is in parent class.
object1.func2()                                     #This function is in child 1.
object2.func1()                                     #This function is in parent class.
object2.func3()                                     #This function is in child 2.

##### Hybrid Inheritance Example #####
class School:
	def func1(self):
		print("This function is in school.")

class Student1(School):
	def func2(self):
		print("This function is in student 1. ")

class Student2(School):
	def func3(self):
		print("This function is in student 2.")

class Student3(Student1, School):
	def func4(self):
		print("This function is in student 3.")

# Driver's code
object = Student3()
object.func1()                                      #This function is in school.
object.func2()                                      #This function is in student 1.
5. Polymorphism

It allows objects of different classes to be treated as objects of a common parent class. It enables a single interface to handle different types of objects, fostering flexibility and reusability in code.

             Type of polymorphism :
    1. Compile-Time Polymorphism: Achieved through method overloading (not natively supported in Python but can be mimicked).
    2. Run-Time Polymorphism: Achieved through method overriding, where a child class provides a specific implementation of a method defined in the parent class.
##### Method overriding Example #####
        class Shape:
    def area(self):
        return 0

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

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

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

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

shapes = [Rectangle(4, 5), Circle(3)]

for shape in shapes:
    print(f"Area: {shape.area()}")                      # Output: Area: 20 Area: 28.26

##### Method overloading Example #####
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    def __str__(self):
        return f"({self.x}, {self.y})"

point1 = Point(1, 2)
point2 = Point(3, 4)
result = point1 + point2

print(result)                                           # Output: (4, 6)

6. Abstraction

Data abstraction is one of the most essential concepts of Python OOPs which is used to hide irrelevant details from the user and show the details that are relevant to the users. Using abstract base classes (from the abc module) we can implement abstraction.

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

circle = Circle(5)
print(circle.area())                    # Output: 78.5