These notes serve as a complete guide to OOP concepts with detailed explanations and examples in Python.


1. Core Principles of OOP

Object-Oriented Programming is based on four fundamental principles:

1.1 Encapsulation

  • Definition: Bundling data (attributes) and methods (functions) that operate on that data into a single unit (class) and restricting direct access to some of the object’s components.
  • Goal: Protect the integrity of the data and expose only necessary details.

Example:

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private attribute

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
        else:
            raise ValueError("Invalid withdrawal amount")

    def get_balance(self):
        return self.__balance

# Usage
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # 1500

1.2 Abstraction

  • Definition: Hiding complex implementation details and showing only the necessary functionality.
  • Goal: Reduce complexity and increase code usability.

Example:

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 ** 2

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

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

# Usage
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.area())

1.3 Inheritance

  • Definition: Allowing a class to acquire the properties and methods of another class.
  • Goal: Promote code reuse and establish a hierarchical relationship between classes.

Example:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Usage
animals = [Dog("Buddy"), Cat("Whiskers")]
for animal in animals:
    print(animal.speak())

1.4 Polymorphism

  • Definition: The ability to present the same interface for different underlying forms (data types or classes).
  • Goal: Write more generic and reusable code.

Example:

class Bird:
    def fly(self):
        return "Flying high"

class Penguin(Bird):
    def fly(self):
        return "I cannot fly, I swim!"

# Usage
birds = [Bird(), Penguin()]
for bird in birds:
    print(bird.fly())

2. Key Concepts in OOP

2.1 Classes and Objects

  • Class: A blueprint for creating objects.
  • Object: An instance of a class containing data (attributes) and methods (functions).

Example:

class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    def display_info(self):
        return f"Car: {self.brand} {self.model}"

# Usage
car = Car("Toyota", "Corolla")
print(car.display_info())

2.2 Access Modifiers

  • Public: Accessible from anywhere.
  • Protected: Accessible within the class and its subclasses (convention: _attribute).
  • Private: Accessible only within the class (convention: __attribute).

Example:

class Example:
    def __init__(self):
        self.public = "Public"
        self._protected = "Protected"
        self.__private = "Private"

    def get_private(self):
        return self.__private

# Usage
obj = Example()
print(obj.public)       # Public
print(obj._protected)   # Protected (not recommended)
print(obj.get_private())  # Accessing private attribute via method

2.3 Method Overloading and Overriding

  • Overloading: Same method name but different parameters (Python doesn’t natively support it; mimic it using default arguments).
  • Overriding: Redefining a method in a subclass that exists in the parent class.

Overloading Example:

def add(a, b, c=0):
    return a + b + c

# Usage
print(add(2, 3))      # 5
print(add(2, 3, 4))   # 9

Overriding Example:

class Parent:
    def greet(self):
        return "Hello from Parent"

class Child(Parent):
    def greet(self):
        return "Hello from Child"

# Usage
obj = Child()
print(obj.greet())  # Hello from Child

2.4 Composition vs Inheritance

  • Inheritance: “Is-a” relationship (e.g., Dog is an Animal).
  • Composition: “Has-a” relationship (e.g., Car has an Engine).

Composition Example:

class Engine:
    def start(self):
        return "Engine started"

class Car:
    def __init__(self):
        self.engine = Engine()

    def start(self):
        return self.engine.start()

# Usage
car = Car()
print(car.start())  # Engine started

3. SOLID Principles in OOP

3.1 Single Responsibility Principle (SRP)

  • A class should have only one reason to change.

Example:

class ReportGenerator:
    def generate_report(self, data):
        return f"Report: {data}"

class ReportPrinter:
    def print_report(self, report):
        print(report)

3.2 Open/Closed Principle (OCP)

  • Classes should be open for extension but closed for modification.

Example:

class Discount:
    def calculate(self, amount):
        return amount

class SeasonalDiscount(Discount):
    def calculate(self, amount):
        return amount * 0.9

class LoyaltyDiscount(Discount):
    def calculate(self, amount):
        return amount * 0.8

# Usage
discounts = [SeasonalDiscount(), LoyaltyDiscount()]
for discount in discounts:
    print(discount.calculate(100))

3.3 Liskov Substitution Principle (LSP)

  • Subtypes should be substitutable for their base types.

Example:

class Bird:
    def fly(self):
        return "Flying"

class Sparrow(Bird):
    pass

class Ostrich(Bird):
    def fly(self):
        raise NotImplementedError("Ostriches cannot fly")

3.4 Interface Segregation Principle (ISP)

  • Avoid forcing classes to implement methods they do not use.

Example:

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print(self):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan(self):
        pass

class MultiFunctionPrinter(Printer, Scanner):
    def print(self):
        return "Printing"

    def scan(self):
        return "Scanning"

3.5 Dependency Inversion Principle (DIP)

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

Example:

class Database:
    def save(self, data):
        pass

class MySQLDatabase(Database):
    def save(self, data):
        print(f"Saving {data} in MySQL")

class Application:
    def __init__(self, db: Database):
        self.db = db

    def save_data(self, data):
        self.db.save(data)

# Usage
app = Application(MySQLDatabase())
app.save_data("User Data")