IT243 Programming with Python

Programming with PythonUnit 79 min read

OOP in Python: Classes, Objects, Inheritance & Polymorphism

Unit 7 of Programming with Python covers object-oriented programming (OOP) principles in Python, including classes, objects, inheritance, polymorphism, encapsulation, and abstraction, with real-world examples and visual traces of code execution.

TAKEAWAYS:

  • OOP organizes code into reusable objects (instances of classes) that bundle data (attributes) and behavior (methods).
  • Inheritance allows child classes to reuse parent class code, reducing redundancy (e.g., Vehicle → Car, Bike).
  • Polymorphism lets objects of different classes respond to the same method call in their own way (e.g., draw() for Circle vs. Square).
  • Python uses __init__ for constructors, @staticmethod for utility methods, and @classmethod for class-level operations.
  • Encapsulation hides internal state (use _ prefix for "protected" attributes) and exposes controlled access via methods.
  • OOP is used in real-world apps (e.g., eSewa’s transaction system, Daraz’s product catalog) to model complex systems modularly.

1. Introduction to OOP Concepts

OOP is a programming paradigm that models real-world entities as objects with attributes (data) and methods (functions). Key concepts:

  • Class: A blueprint for creating objects (e.g., class BankAccount).
  • Object: An instance of a class (e.g., my_account = BankAccount()).
  • Attribute: Data stored in an object (e.g., balance).
  • Method: Functions defined inside a class (e.g., deposit()).

Why OOP?

  • Modularity: Code is organized into reusable components.
  • Scalability: Easy to extend (e.g., adding new features to BankAccount).
  • Maintainability: Logic is encapsulated, reducing bugs.

2. Classes and Objects

Defining a Class

class BankAccount:
    def __init__(self, account_holder, balance=0):
        self.account_holder = account_holder  # Attribute
        self.balance = balance

    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 Objects

# Create an object
account1 = BankAccount("Ramesh", 1000)
account2 = BankAccount("Sita", 500)

# Call methods
account1.deposit(200)  # Output: Deposited $200. New balance: $1200
account2.withdraw(100) # Output: Withdrew $100. New balance: $400

Visual: Object Creation

classDiagram
    class BankAccount {
        -account_holder: str
        -balance: float
        +__init__(account_holder, balance)
        +deposit(amount)
        +withdraw(amount)
    }
    BankAccount <|-- account1
    BankAccount <|-- account2

3. The __init__ Method (Constructor)

  • Called automatically when an object is created.
  • Initializes object attributes.
  • Example: BankAccount uses __init__ to set account_holder and balance.

Trace: Object Initialization

Step Code Executed account1 State
1 account1 = BankAccount("Ramesh", 1000) account_holder="Ramesh", balance=1000
2 __init__ runs Attributes initialized

4. Inheritance

Allows a child class to inherit attributes/methods from a parent class. Example: SavingsAccount inherits from BankAccount.

class SavingsAccount(BankAccount):
    def __init__(self, account_holder, balance=0, interest_rate=0.05):
        super().__init__(account_holder, balance)  # Call parent's __init__
        self.interest_rate = interest_rate

    def add_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest
        print(f"Added ${interest:.2f} interest. New balance: ${self.balance:.2f}")

Visual: Inheritance Hierarchy

classDiagram
    class BankAccount {
        +__init__(account_holder, balance)
        +deposit(amount)
        +withdraw(amount)
    }
    class SavingsAccount {
        +__init__(account_holder, balance, interest_rate)
        +add_interest()
    }
    SavingsAccount <|-- BankAccount

Real-World Example: eSewa’s Transaction System

  • Parent Class: Transaction
    • Attributes: amount, sender, receiver
    • Methods: process(), validate()
  • Child Classes:
    • ElectricityPayment(Transaction): Adds billing_id and due_date.
    • MobileRecharge(Transaction): Adds network and plan_type.
  • Why? Reuses common logic (e.g., validate()) while customizing for each payment type.

5. Polymorphism

Same method name behaves differently based on the object. Example: draw() for Circle vs. Square.

class Shape:
    def draw(self):
        raise NotImplementedError("Subclass must implement this!")

class Circle(Shape):
    def draw(self):
        print("Drawing a circle")

class Square(Shape):
    def draw(self):
        print("Drawing a square")

# Polymorphic behavior
shapes = [Circle(), Square()]
for shape in shapes:
    shape.draw()

Output:

Drawing a circle
Drawing a square

Visual: Polymorphism in Action

flowchart TD
    A["shape.draw()"] -->|"Circle"| B["Print 'Drawing a circle'"]
    A -->|"Square"| C["Print 'Drawing a square'"]

Real-World Example: Daraz’s Product Catalog

  • Parent Class: Product
    • Method: display()
  • Child Classes:
    • Electronics(Product): Overrides display() to show specs (e.g., RAM, storage).
    • Clothing(Product): Overrides display() to show size/color.
  • Why? Daraz’s app calls product.display() for any product, and the correct format appears.

6. Encapsulation and Abstraction

  • Encapsulation: Hides data and exposes methods to interact with it.
    • Use _ prefix for "protected" attributes (e.g., _balance).
  • Abstraction: Shows only essential features (e.g., BankAccount.withdraw() hides internal checks).

Example: Protected Attributes

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # Protected attribute

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

Visual: Encapsulation

classDiagram
    class BankAccount {
        -_balance: float
        +withdraw(amount)
    }
    note for BankAccount "External code cannot access _balance directly."

7. Special Methods (__str__, __repr__)

  • __str__: Returns a user-friendly string (e.g., for print()).
  • __repr__: Returns a developer-friendly string (e.g., for debugging).
class BankAccount:
    def __str__(self):
        return f"Account(holder='{self.account_holder}', balance=${self.balance})"

    def __repr__(self):
        return f"BankAccount('{self.account_holder}', {self.balance})"

account = BankAccount("Ramesh", 1000)
print(account)  # Uses __str__: Account(holder='Ramesh', balance=$1000)

8. Class Methods and Static Methods

  • @classmethod: Operates on the class, not the instance. Example: from_csv() to create objects from a file.
  • @staticmethod: Utility function unrelated to class/instance. Example: validate_amount(amount).
class BankAccount:
    @classmethod
    def from_csv(cls, data):
        return cls(data["holder"], data["balance"])

    @staticmethod
    def validate_amount(amount):
        return amount >= 0

# Usage
data = {"holder": "Sita", "balance": 500}
account = BankAccount.from_csv(data)

9. Composition vs. Inheritance

Feature Inheritance Composition
Relationship "Is-a" (e.g., SavingsAccount is a BankAccount) "Has-a" (e.g., Car has a Engine)
Use Case Extend functionality Reuse existing classes
Flexibility Less flexible (tight coupling) More flexible (loose coupling)

Example: Composition

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

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

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

10. Real-World Applications in Nepal

  1. eSewa:

    • OOP Use: User, Transaction, PaymentGateway classes.
    • Polymorphism: Different payment methods (UPI, CreditCard) implement process_payment().
  2. Ncell’s Billing System:

    • Inheritance: Customer → PrepaidCustomer, PostpaidCustomer.
    • Encapsulation: Hides balance and exposes recharge()/pay_bill().
  3. NEPSE Stock Market:

    • Composition: Stock has a TradingHistory object.
    • Abstraction: Trade class hides complex order-matching logic.

Exam Tip

  1. Code Tracing: Expect questions on how objects are created and methods called (e.g., "What is the output of account.withdraw(200)?").
  2. Diagrams: Draw class diagrams for inheritance/polymorphism (e.g., Vehicle → Car, Bike).
  3. Error Spotting: Identify missing __init__, incorrect super() calls, or broken encapsulation.
  4. Real-World Links: Relate OOP to apps like eSewa/Khalti (e.g., "How would you model a User class?").
  5. Short Answer: Define terms like inheritance, polymorphism, and encapsulation concisely.

Practice Question: Write a Library class with Book and Member subclasses. Include:

  • A Book with title, author, and is_checked_out.
  • A Member with name and borrowed_books (a list).
  • Methods: check_out_book(), return_book(). Trace: Show the state of member1.borrowed_books after checking out 2 books.

Based on the TU BIM syllabus for Programming with Python (IT243), unit 7.

Discussion

Loading…