📓 Python Programming - Object-Oriented Programming (OOP)


1. Classes & Objects

What is OOP?

  • Organizes code around real-world entities (objects).

  • Class: Blueprint for creating objects.

  • Object: An instance of a class.

# Defining a class
class Student:
    pass

# Creating an object
s1 = Student()
print(type(s1))

2. Class vs Object Attributes

  • Class attributes: Shared by all objects.

  • Object attributes: Unique to each object.

➤ Example:

class Student:
    college = "Westcliff University"  # Class attribute

    def __init__(self, name, age):
        self.name = name  # Object attribute
        self.age = age

s1 = Student("Alice", 20)
s2 = Student("Bob", 22)

print(s1.name, s1.college)
print(s2.name, s2.college)

3. Initializing Object Attributes with __init__()

__init__() is a special method that runs when an object is created. (Like Constructor)

➤ Example:

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

c1 = Car("Toyota", 2020)
print(c1.brand, c1.year)

4. The self Keyword

  • Refers to the current object.

  • Used to access object attributes and methods.

➤ Example:

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

    def speak(self):
        print(f"{self.name} says Woof!")

d = Dog("Buddy")
d.speak()

5. Inheritance

Inheritance allows a class to use properties and methods of another class.

Types: - Single Inheritance

  • Multiple Inheritance

  • Multi-Level Inheritance

➤ Single Inheritance:

class Animal:
    def speak(self):
        print("Animal sound")

class Dog(Animal):
    def bark(self):
        print("Woof!")

d = Dog()
d.speak()
d.bark()

➤ Multiple Inheritance:

class A:
    def show_a(self):
        print("A")

class B:
    def show_b(self):
        print("B")

class C(A, B):
    pass

c = C()
c.show_a()
c.show_b()

➤ Multi-Level Inheritance:

class Grandparent:
    def grandparent_func(self):
        print("Grandparent")

class Parent(Grandparent):
    def parent_func(self):
        print("Parent")

class Child(Parent):
    def child_func(self):
        print("Child")

c = Child()
c.grandparent_func()
c.parent_func()
c.child_func()

6. Polymorphism & Operator Overloading

Polymorphism:

  • Same function name but different behaviors.

Operator Overloading:

  • Using special methods to define how operators behave for custom objects.

➤ Polymorphism Example:

class Cat:
    def sound(self):
        print("Meow")

class Dog:
    def sound(self):
        print("Woof")

animals = [Cat(), Dog()]
for animal in animals:
    animal.sound()

Traditional Overloading Does Not Work in Python

def greet(name):
    print(f"Hello, {name}!")

def greet(name, age):
    print(f"Hello, {name}! You are {age} years old.")

greet("Alice")  # ❌ TypeError: greet() missing 1 required positional argument: 'age'

➤ Python's Way of Function Overloading:

Python does not support function overloading in the traditional sense, as seen in languages like C++ or Java. In those languages, you can have multiple functions with the same name but different parameters (type or number). Python, however, allows you to define a function only once. If you define it again, it simply overwrites the previous definition.

But there is some work around this. Discuss how to implement function overloading in Python using default arguments or variable-length arguments.

# Using default arguments for overloading-like behavior
def greet(name, age=None):
    if age is None:
        print(f"Hello, {name}!")
    else:
        print(f"Hello, {name}! You are {age} years old.")

greet("Alice")      # Works!
greet("Bob", 25)    # Also works!

# Using *args for variable arguments
def add(*numbers):
    return sum(numbers)

print(add(1, 2))        # 3
print(add(1, 2, 3, 4))  # 10

➤ Operator Overloading Example:

class Point:
    def __init__(self, x):
        self.x = x

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

p1 = Point(5)
p2 = Point(10)
result = p1 + p2
print(result.x)

7. Function Overriding & Encapsulation

Function Overriding:

  • Child class redefines a method from the parent class.

Encapsulation:

  • Restricts direct access to variables using private/protected attributes.

➤ Function Overriding Example:

class Animal:
    def speak(self):
        print("Animal sound")

class Dog(Animal):
    def speak(self):
        print("Woof!")

d = Dog()
d.speak()

➤ Encapsulation Example:

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

    def get_balance(self):
        return self.__balance

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

acc = BankAccount(1000)
acc.deposit(500)
print(acc.get_balance())

➤ Using Properties (Getters & Setters):

class Temperature:
    def __init__(self):
        self._celsius = 0

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero is not possible")
        self._celsius = value

    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32

# Usage
temp = Temperature()
temp.celsius = 25
print(f"Celsius: {temp.celsius}")
print(f"Fahrenheit: {temp.fahrenheit}")

8. Class Methods & Static Methods

Class Methods & Static Methods:

  • Instance Method: Needs self, works with object data
  • Class Method: Uses @classmethod, gets cls parameter
  • Static Method: Uses @staticmethod, independent function inside class

➤ Example:

class Person:
    species = "Homo sapiens"  # Class attribute

    def __init__(self, name, age):
        self.name = name        # Instance attribute
        self.age = age

    # Instance method
    def introduce(self):
        return f"Hi, I'm {self.name}, {self.age} years old"

    # Class method
    @classmethod
    def get_species(cls):
        return cls.species

    @classmethod
    def create_baby(cls, name):
        return cls(name, 0)  # Alternative constructor

    # Static method
    @staticmethod
    def is_adult(age):
        return age >= 18

# Usage examples
p1 = Person("Alice", 25)
print(p1.introduce())           # Instance method

print(Person.get_species())     # Class method
baby = Person.create_baby("Bob") # Class method as constructor
print(baby.introduce())

print(Person.is_adult(17))      # Static method
print(Person.is_adult(20))

9. Special Methods (Magic Methods)

Special methods start and end with double underscores __

Common ones: - __str__() → What users see when printing - __repr__() → Developer representation
- __len__() → Makes objects work with len() - __eq__() → Custom equality comparison

➤ Example:

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __str__(self):
        return f"Book: {self.title}"

    def __repr__(self):
        return f"Book('{self.title}', {self.pages})"

    def __len__(self):
        return self.pages

    def __eq__(self, other):
        return self.title == other.title

# Testing special methods
book1 = Book("Python Guide", 300)
book2 = Book("Python Guide", 350)

print(book1)                    # Uses __str__
print(repr(book1))              # Uses __repr__
print(len(book1))               # Uses __len__
print(book1 == book2)           # Uses __eq__

# Create a list of books
books = [book1, Book("Java Guide", 400)]
print(f"Total pages: {sum(len(book) for book in books)}")

10. Composition vs Inheritance

Two ways to reuse code:

  • Inheritance: "IS-A" relationship (Dog IS-A Animal)
  • Composition: "HAS-A" relationship (Car HAS-A Engine)

➤ Composition Example:

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

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

class Car:
    def __init__(self, brand, engine):
        self.brand = brand
        self.engine = engine  # Car HAS-A Engine

    def start_car(self):
        return f"{self.brand}: {self.engine.start()}"

# Usage
engine = Engine(200)
car = Car("Toyota", engine)
print(car.start_car())

# Compare with inheritance approach
class Animal:
    def breathe(self):
        return "Breathing..."

class Dog(Animal):  # Dog IS-A Animal
    def bark(self):
        return "Woof!"

dog = Dog()
print(dog.breathe())  # Inherited method
print(dog.bark())     # Own method

🎯 Quick Recap

✅ OOP models real-world entities with classes and objects

✅ Class attributes shared, object attributes unique

✅ Inheritance promotes code reuse (IS-A relationship)

✅ Composition provides flexibility (HAS-A relationship)

✅ Polymorphism enables flexibility through method overriding

✅ Properties provide clean encapsulation with getters/setters

✅ Class methods and static methods offer different functionality levels

✅ Special methods make objects work with built-in functions

✅ Function overriding & encapsulation provide safety and control

📝 Practice Exercises

Basic Level:

  1. Create a Car class with brand and year, and display details using a method.

  2. Demonstrate single inheritance with Animal → Bird.

  3. Create a class Rectangle that overloads + to add areas of two rectangles.

  4. Use encapsulation to hide a bank account's balance and provide methods to deposit/withdraw.

  5. Demonstrate function overriding using a parent-child class structure.

Intermediate Level:

  1. Create a Person class with properties for name and age validation.

  2. Build a Calculator class with static methods for basic operations.

  3. Create a Book class with special methods (__str__, __len__, __eq__).

  4. Use composition to create a Computer class that HAS-A Processor.

  5. Create a class method that works as an alternative constructor.

Advanced Level:

  1. Build a complete BankAccount system with properties and validation.

  2. Create a polymorphic system with different Shape classes.

  3. Implement a Library class that manages Book objects using composition.

  4. Design a class hierarchy for different types of vehicles with proper inheritance.

  5. Create a Temperature class with property decorators for Celsius/Fahrenheit conversion.