OOP

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects. Objects are instances of classes, which can have attributes (data) and methods (functions). OOP helps organize code, making it reusable and easier to understand.

Key Concepts of OOP

  1. Class: A blueprint for creating objects. It defines attributes and methods.
  2. Object: An instance of a class.
  3. Attributes: Variables that belong to a class or object.
  4. Methods: Functions defined inside a class that operate on the object’s attributes.
  5. Inheritance: Allows a class to inherit attributes and methods from another class.
  6. Polymorphism: Allows methods in different classes to have the same name but behave differently.
  7. Encapsulation: Hides the internal details of an object and only exposes necessary parts.
  8. Abstraction: Hides complex implementation details and shows only essential features.

Defining a Class

A class is created using the class keyword.

Example:

Python
Copy
class Person:
    # Class attribute
    species = "Human"
    
    # Constructor method to initialize object attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # Method to display details
    def greet(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

Creating an Object

An object is created by calling the class like a function.

Example:

Python
Copy
# Creating an object of the Person class
person1 = Person("Alice", 25)

# Accessing attributes and methods
print(person1.species)  # Output: Human
print(person1.name)     # Output: Alice
person1.greet()         # Output: Hello, my name is Alice and I am 25 years old.

Inheritance

Inheritance allows a class (child) to inherit the attributes and methods of another class (parent).

Example:

Python
Copy
# Parent class
class Animal:
    def __init__(self, name):
        self.name = name

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

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

# Creating objects
animal = Animal("Generic Animal")
animal.speak()  # Output: Generic Animal makes a sound.

dog = Dog("Buddy")
dog.speak()     # Output: Buddy barks.

Polymorphism

Polymorphism allows methods to have the same name but behave differently depending on the class.

Example:

Python
Copy
class Cat(Animal):
    def speak(self):
        print(f"{self.name} meows.")

# Using polymorphism
animals = [Dog("Buddy"), Cat("Kitty")]

for animal in animals:
    animal.speak()

Encapsulation

Encapsulation is achieved by making attributes private (using _ or __) and providing public methods to access or modify them.

Example:

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

    def deposit(self, amount):
        self.__balance += amount
        print(f"Deposited {amount}. New balance: {self.__balance}")

    def withdraw(self, amount):
        if amount > self.__balance:
            print("Insufficient funds!")
        else:
            self.__balance -= amount
            print(f"Withdrew {amount}. New balance: {self.__balance}")

# Creating an object
account = BankAccount("Alice", 1000)
account.deposit(500)        # Output: Deposited 500. New balance: 1500
account.withdraw(2000)      # Output: Insufficient funds!

Abstraction

Abstraction is implemented using abstract classes, which are classes that cannot be instantiated directly. Use the abc module for abstraction.

Example:

Python
Copy
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    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)

# Creating an object
rect = Rectangle(5, 10)
print("Area:", rect.area())         # Output: Area: 50
print("Perimeter:", rect.perimeter())  # Output: Perimeter: 30

Special Methods (Magic Methods)

Special methods (also called magic or dunder methods) allow you to define how objects behave with operators like +, ==, etc.

Example:

Python
Copy
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"Point({self.x}, {self.y})"

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
print(p3)  # Output: Point(4, 6)

Class Methods

  • Class methods are methods that belong to the class, not the instance of the class.
  • They are defined using the @classmethod decorator.
  • Instead of self, they take cls as the first parameter, which represents the class itself.
  • Useful for modifying or accessing class-level attributes.

Example:

Python
Copy
class Person:
    count = 0  # Class attribute

    @classmethod
    def increment_count(cls):
        cls.count += 1

# Using the class method
Person.increment_count()
print(Person.count)  # Output: 1

When to Use Class Methods?

  • When you need to work with class attributes or create alternative constructors.

Static Methods

  • Static methods do not depend on the instance or class.
  • They are defined using the @staticmethod decorator.
  • They behave like regular functions but are defined inside a class for logical grouping.

Example:

Python
Copy
class Math:
    @staticmethod
    def add(a, b):
        return a + b

# Using the static method
result = Math.add(3, 5)
print(result)  # Output: 8

When to Use Static Methods?

  • When you need utility methods that don’t depend on class or instance data.

Data Classes

  • Introduced in Python 3.7 via the dataclasses module.
  • Data classes simplify creating classes used for storing data.
  • They automatically generate methods like __init__, __repr__, and __eq__.

How to Create a Data Class?

Use the @dataclass decorator.

Example:

Python
Copy
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

# Creating an object of the data class
p = Person("Alice", 30)
print(p.name)  # Output: Alice
print(p)       # Output: Person(name='Alice', age=30)

When to Use Data Classes?

When you need simple classes to store and manage data without writing boilerplate code.