Python Topic

Python Object-Oriented Programming (OOP)

1. Encapsulation

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

2. Inheritance

Creating new classes (child classes) from existing ones (parent classes), inheriting attributes and methods.

3. Polymorphism

Ability to present the same interface for different underlying forms (data types).

4. Abstraction

Hiding complex implementation details and showing only essential features.


Classes and Objects

Basic Class Definition

python

class Dog:
    # Class attribute - shared by all instances
    species = "Canis familiaris"
    
    # Constructor method - called when object is created
    def __init__(self, name, age, breed):
        # Instance attributes - unique to each instance
        self.name = name
        self.age = age
        self.breed = breed
        self._is_sleeping = False  # Protected attribute (convention)
    
    # Instance method
    def bark(self):
        return f"{self.name} says Woof!"
    
    def sleep(self):
        self._is_sleeping = True
        return f"{self.name} is sleeping"
    
    def wake_up(self):
        self._is_sleeping = False
        return f"{self.name} woke up"
    
    # String representation
    def __str__(self):
        return f"{self.name} is a {self.age}-year-old {self.breed}"
    
    # Formal string representation
    def __repr__(self):
        return f"Dog('{self.name}', {self.age}, '{self.breed}')"

# Creating objects (instances)
dog1 = Dog("Buddy", 3, "Golden Retriever")
dog2 = Dog("Max", 5, "German Shepherd")

print(dog1.bark())  # Buddy says Woof!
print(dog2)         # Max is a 5-year-old German Shepherd
print(f"Species: {Dog.species}")  # Accessing class attribute

Encapsulation in Detail

python

class BankAccount:
    def __init__(self, account_holder, initial_balance=0):
        self.account_holder = account_holder
        self._account_number = self._generate_account_number()  # Protected
        self.__balance = initial_balance  # Private (name mangling)
    
    def _generate_account_number(self):
        """Protected method - for internal use"""
        import random
        return f"ACC{random.randint(10000, 99999)}"
    
    def deposit(self, amount):
        """Public method to deposit money"""
        if amount > 0:
            self.__balance += amount
            return f"Deposited ${amount}. New balance: ${self.__balance}"
        return "Invalid deposit amount"
    
    def withdraw(self, amount):
        """Public method to withdraw money"""
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return f"Withdrew ${amount}. New balance: ${self.__balance}"
        return "Insufficient funds or invalid amount"
    
    # Getter method for balance (controlled access)
    def get_balance(self):
        return self.__balance
    
    # Property decorator - Pythonic way for getters/setters
    @property
    def balance(self):
        return self.__balance
    
    @property
    def account_info(self):
        return f"Account: {self._account_number}, Holder: {self.account_holder}"

# Usage
account = BankAccount("John Doe", 1000)
print(account.deposit(500))      # Deposited $500. New balance: $1500
print(account.withdraw(200))     # Withdrew $200. New balance: $1300
print(account.get_balance())     # 1300
print(account.balance)           # 1300 (using property)
print(account.account_info)      # Account: ACCXXXXX, Holder: John Doe

# Trying to access private attribute directly (will fail)
# print(account.__balance)  # AttributeError

Inheritance in Detail

Single Inheritance

python

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def speak(self):
        raise NotImplementedError("Subclass must implement this method")
    
    def move(self):
        return f"{self.name} is moving"
    
    def __str__(self):
        return f"{self.name} the {self.species}"

class Cat(Animal):  # Inherits from Animal
    def __init__(self, name, breed, is_indoor=True):
        super().__init__(name, "Cat")  # Call parent constructor
        self.breed = breed
        self.is_indoor = is_indoor
    
    # Method overriding
    def speak(self):
        return "Meow!"
    
    # Additional method specific to Cat
    def purr(self):
        return "Purrrrr..."
    
    def __str__(self):
        return f"{super().__str__()} ({self.breed})"

class Dog(Animal):  # Inherits from Animal
    def __init__(self, name, breed):
        super().__init__(name, "Dog")
        self.breed = breed
    
    def speak(self):
        return "Woof!"
    
    def fetch(self):
        return f"{self.name} is fetching the ball"

# Usage
cat = Cat("Whiskers", "Siamese")
dog = Dog("Buddy", "Labrador")

print(cat.speak())      # Meow!
print(dog.speak())      # Woof!
print(cat.move())       # Whiskers is moving
print(cat.purr())       # Purrrrr...
print(dog.fetch())      # Buddy is fetching the ball
print(cat)              # Whiskers the Cat (Siamese)

Multiple Inheritance

python

class Flyable:
    def __init__(self, max_altitude):
        self.max_altitude = max_altitude
        self.is_flying = False
    
    def take_off(self):
        self.is_flying = True
        return "Taking off"
    
    def land(self):
        self.is_flying = False
        return "Landing"
    
    def fly(self):
        return "Flying high"

class Swimmable:
    def __init__(self, max_depth):
        self.max_depth = max_depth
        self.is_swimming = False
    
    def dive(self):
        self.is_swimming = True
        return "Diving into water"
    
    def surface(self):
        self.is_swimming = False
        return "Surfacing"
    
    def swim(self):
        return "Swimming"

class Duck(Animal, Flyable, Swimmable):  # Multiple inheritance
    def __init__(self, name):
        Animal.__init__(self, name, "Duck")
        Flyable.__init__(self, 1000)    # 1000 meters max altitude
        Swimmable.__init__(self, 10)    # 10 meters max depth
    
    def speak(self):
        return "Quack!"
    
    def display_abilities(self):
        return f"I can: {self.fly()} and {self.swim()}"

# Usage
duck = Duck("Donald")
print(duck.speak())           # Quack!
print(duck.take_off())        # Taking off
print(duck.fly())             # Flying high
print(duck.dive())            # Diving into water
print(duck.display_abilities()) # I can: Flying high and Swimming

Polymorphism in Detail

Method Overriding

python

class Shape:
    def area(self):
        raise NotImplementedError("Subclass must implement area()")
    
    def perimeter(self):
        raise NotImplementedError("Subclass must implement perimeter()")
    
    def __str__(self):
        return self.__class__.__name__

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

class Triangle(Shape):
    def __init__(self, base, height, side1, side2, side3):
        self.base = base
        self.height = height
        self.side1 = side1
        self.side2 = side2
        self.side3 = side3
    
    def area(self):
        return 0.5 * self.base * self.height
    
    def perimeter(self):
        return self.side1 + self.side2 + self.side3

# Polymorphic function - works with any Shape
def print_shape_info(shape):
    print(f"{shape}: Area = {shape.area():.2f}, Perimeter = {shape.perimeter():.2f}")

# Usage
shapes = [
    Rectangle(5, 3),
    Circle(4),
    Triangle(6, 4, 3, 4, 5)
]

for shape in shapes:
    print_shape_info(shape)
# Output:
# Rectangle: Area = 15.00, Perimeter = 16.00
# Circle: Area = 50.27, Perimeter = 25.13
# Triangle: Area = 12.00, Perimeter = 12.00

Operator Overloading

python

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Operator overloading
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def magnitude(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

# Usage
v1 = Vector(2, 3)
v2 = Vector(1, 4)

v3 = v1 + v2  # Uses __add__
v4 = v1 * 3   # Uses __mul__

print(v1)           # Vector(2, 3)
print(v2)           # Vector(1, 4)
print(v3)           # Vector(3, 7)
print(v4)           # Vector(6, 9)
print(v1.magnitude()) # 3.605551275463989
print(v1 == v2)     # False

Advanced OOP Concepts

Abstract Base Classes

python

from abc import ABC, abstractmethod

class Employee(ABC):
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
    
    @abstractmethod
    def calculate_salary(self):
        pass
    
    @abstractmethod
    def get_role(self):
        pass
    
    def get_info(self):
        return f"ID: {self.employee_id}, Name: {self.name}"

class FullTimeEmployee(Employee):
    def __init__(self, name, employee_id, monthly_salary):
        super().__init__(name, employee_id)
        self.monthly_salary = monthly_salary
    
    def calculate_salary(self):
        return self.monthly_salary
    
    def get_role(self):
        return "Full-time Employee"

class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate, hours_worked):
        super().__init__(name, employee_id)
        self.hourly_rate = hourly_rate
        self.hours_worked = hours_worked
    
    def calculate_salary(self):
        return self.hourly_rate * self.hours_worked
    
    def get_role(self):
        return "Part-time Employee"

class Contractor(Employee):
    def __init__(self, name, employee_id, project_fee):
        super().__init__(name, employee_id)
        self.project_fee = project_fee
    
    def calculate_salary(self):
        return self.project_fee
    
    def get_role(self):
        return "Contractor"

# Usage
employees = [
    FullTimeEmployee("Alice", "FT001", 5000),
    PartTimeEmployee("Bob", "PT001", 25, 80),
    Contractor("Charlie", "C001", 10000)
]

for emp in employees:
    print(f"{emp.get_info()} - {emp.get_role()} - Salary: ${emp.calculate_salary()}")

Class Methods and Static Methods

python

class Student:
    # Class attribute
    school_name = "ABC High School"
    total_students = 0
    
    def __init__(self, name, grade, student_id):
        self.name = name
        self.grade = grade
        self.student_id = student_id
        Student.total_students += 1  # Modify class attribute
    
    # Instance method
    def get_info(self):
        return f"{self.name} (ID: {self.student_id}), Grade: {self.grade}"
    
    # Class method - works with class, not instance
    @classmethod
    def get_total_students(cls):
        return cls.total_students
    
    @classmethod
    def create_from_string(cls, student_str):
        """Alternative constructor"""
        name, grade, student_id = student_str.split(",")
        return cls(name, int(grade), student_id)
    
    # Static method - doesn't need class or instance
    @staticmethod
    def is_valid_grade(grade):
        return 1 <= grade <= 12
    
    @staticmethod
    def get_school_motto():
        return "Strive for Excellence"

# Usage
student1 = Student("John", 10, "S001")
student2 = Student("Sarah", 11, "S002")

# Using class method
print(f"Total students: {Student.get_total_students()}")  # Total students: 2

# Using alternative constructor
student3 = Student.create_from_string("Mike,9,S003")
print(student3.get_info())  # Mike (ID: S003), Grade: 9

# Using static methods
print(Student.is_valid_grade(15))  # False
print(Student.get_school_motto())  # Strive for Excellence

Property Decorators for Getters/Setters

python

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        """Getter for celsius"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """Setter for celsius with validation"""
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero!")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """Computed property - fahrenheit"""
        return (self._celsius * 9/5) + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Setter for fahrenheit"""
        self._celsius = (value - 32) * 5/9
    
    @property
    def kelvin(self):
        """Computed property - kelvin"""
        return self._celsius + 273.15

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

# Using setters
temp.celsius = 30
print(f"New Fahrenheit: {temp.fahrenheit}") # New Fahrenheit: 86.0

temp.fahrenheit = 100
print(f"New Celsius: {temp.celsius}")       # New Celsius: 37.777...
Real-World Example: E-commerce System

python

class Product:
    def __init__(self, product_id, name, price, quantity):
        self.product_id = product_id
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def update_quantity(self, amount):
        self.quantity += amount
    
    def __str__(self):
        return f"{self.name} - ${self.price} (Stock: {self.quantity})"

class Customer:
    def __init__(self, customer_id, name, email):
        self.customer_id = customer_id
        self.name = name
        self.email = email
        self._orders = []
    
    def place_order(self, cart):
        order = Order(self, cart.items)
        self._orders.append(order)
        return order
    
    def get_order_history(self):
        return self._orders
    
    def __str__(self):
        return f"Customer: {self.name} ({self.email})"

class Cart:
    def __init__(self):
        self.items = {}  # {product: quantity}
    
    def add_item(self, product, quantity=1):
        if product.quantity >= quantity:
            if product in self.items:
                self.items[product] += quantity
            else:
                self.items[product] = quantity
            return True
        return False
    
    def remove_item(self, product, quantity=1):
        if product in self.items:
            if self.items[product] <= quantity:
                del self.items[product]
            else:
                self.items[product] -= quantity
            return True
        return False
    
    def get_total(self):
        return sum(product.price * quantity for product, quantity in self.items.items())
    
    def clear(self):
        self.items.clear()
    
    def __str__(self):
        if not self.items:
            return "Cart is empty"
        items_str = "\n".join([f"{product.name} x{quantity}" for product, quantity in self.items.items()])
        return f"Cart:\n{items_str}\nTotal: ${self.get_total():.2f}"

class Order:
    order_counter = 0
    
    def __init__(self, customer, items):
        Order.order_counter += 1
        self.order_id = f"ORD{Order.order_counter:04d}"
        self.customer = customer
        self.items = items.copy()
        self.total = sum(product.price * quantity for product, quantity in items.items())
        self.status = "Pending"
        
        # Update product quantities
        for product, quantity in items.items():
            product.update_quantity(-quantity)
    
    def complete_order(self):
        self.status = "Completed"
    
    def __str__(self):
        items_str = "\n".join([f"  {product.name} x{quantity}" for product, quantity in self.items.items()])
        return f"Order {self.order_id} - {self.customer.name}\n{items_str}\nTotal: ${self.total:.2f} - Status: {self.status}"

# Usage Example
# Create products
laptop = Product("P001", "Laptop", 999.99, 10)
mouse = Product("P002", "Wireless Mouse", 29.99, 50)
keyboard = Product("P003", "Mechanical Keyboard", 79.99, 25)

# Create customer
customer = Customer("C001", "John Doe", "john@example.com")

# Create cart and add items
cart = Cart()
cart.add_item(laptop, 1)
cart.add_item(mouse, 2)
cart.add_item(keyboard, 1)

print("Before order:")
print(cart)
print(f"Laptop stock: {laptop.quantity}")

# Place order
order = customer.place_order(cart)
order.complete_order()

print("\nAfter order:")
print(order)
print(f"Laptop stock: {laptop.quantity}")
print(f"Cart: {cart}")  # Cart should be empty after order

# Order history
print(f"\nOrder history for {customer.name}:")
for order in customer.get_order_history():
    print(order)

This extensive OOP guide covers all the fundamental concepts with practical examples. The key to mastering OOP is understanding how to properly structure your code using these principles to create maintainable, reusable, and scalable applications.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?