Object-Oriented Programming (OOP) in Python

DONT FORGET TO REPLACE ME LATER

Object-Oriented Programming is a programming paradigm that organizes code into objects, which combine data (attributes) and behavior (methods). Python supports four main OOP principles:

1. Encapsulation

Bundling data and methods that operate on that data within a single unit (class), and restricting direct access to some components.

class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number  # Public attribute
        self.__balance = balance  # Private attribute (name mangling with __)

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return f"Deposited ${amount}. New balance: ${self.__balance}"
        return "Invalid amount"

    def get_balance(self):  # Getter method
        return self.__balance

    def __str__(self):
        return f"Account {self.account_number}: ${self.__balance}"

# Usage
account = BankAccount("123456", 1000)
print(account.deposit(500))  # Deposited $500. New balance: $1500
print(account.get_balance())  # 1500
# print(account.__balance)  # AttributeError - can't access private attribute directly

2. Inheritance

Creating new classes based on existing classes, inheriting their attributes and methods.

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

    def make_sound(self):
        return "Some generic sound"

    def info(self):
        return f"{self.name} is a {self.species}"

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Dog")  # Call parent constructor
        self.breed = breed

    def make_sound(self):  # Override parent method
        return "Woof! Woof!"

    def fetch(self):
        return f"{self.name} is fetching the ball"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color

    def make_sound(self):
        return "Meow!"

# Usage
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

print(dog.info())  # Buddy is a Dog
print(dog.make_sound())  # Woof! Woof!
print(cat.make_sound())  # Meow!

3. Polymorphism

The ability of different classes to be treated as instances of the same class through a common interface.

class Shape:
    def area(self):
        pass

    def perimeter(self):
        pass

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

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

    def perimeter(self):
        return 2 * (self.width + self.height)

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

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

    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Polymorphism in action
def print_shape_info(shape):
    print(f"Area: {shape.area():.2f}")
    print(f"Perimeter: {shape.perimeter():.2f}")

# Usage
rectangle = Rectangle(5, 3)
circle = Circle(4)

print("Rectangle:")
print_shape_info(rectangle)  # Works with Rectangle

print("\nCircle:")
print_shape_info(circle)  # Works with Circle

4. Abstraction

Hiding complex implementation details and showing only essential features of an object.

from abc import ABC, abstractmethod

class Vehicle(ABC):  # Abstract base class
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    @abstractmethod
    def start_engine(self):
        """Must be implemented by subclasses"""
        pass

    @abstractmethod
    def stop_engine(self):
        """Must be implemented by subclasses"""
        pass

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

class Car(Vehicle):
    def start_engine(self):
        return f"{self.brand} car engine started with key"

    def stop_engine(self):
        return f"{self.brand} car engine stopped"

class ElectricCar(Vehicle):
    def start_engine(self):
        return f"{self.brand} electric motor activated silently"

    def stop_engine(self):
        return f"{self.brand} electric motor deactivated"

# Usage
# vehicle = Vehicle("Generic", "Model")  # TypeError - can't instantiate abstract class

car = Car("Toyota", "Camry")
tesla = ElectricCar("Tesla", "Model 3")

print(car.start_engine())  # Toyota car engine started with key
print(tesla.start_engine())  # Tesla electric motor activated silently

Bonus: Composition

Building complex objects by combining simpler ones (alternative to inheritance).

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return f"Engine with {self.horsepower}HP started"

class Wheels:
    def __init__(self, count):
        self.count = count

    def rotate(self):
        return f"{self.count} wheels rotating"

class Car:
    def __init__(self, brand, engine, wheels):
        self.brand = brand
        self.engine = engine  # Composition
        self.wheels = wheels  # Composition

    def drive(self):
        return f"{self.brand}: {self.engine.start()} and {self.wheels.rotate()}"

# Usage
engine = Engine(200)
wheels = Wheels(4)
my_car = Car("Honda", engine, wheels)

print(my_car.drive())  # Honda: Engine with 200HP started and 4 wheels rotating

These principles help create maintainable, reusable, and scalable code!