Programming with PythonUnit 712 min read
OOP in Python: Classes, Objects, Inheritance, Polymorphism & Encapsulation
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 applications and exam-focused examples.
TAKEAWAYS:
- Understand classes and objects as blueprints and instances, and how to define them using
classand__init__methods. - Master inheritance (single, multilevel, multiple, hierarchical) to reuse and extend code efficiently.
- Apply polymorphism (method overriding and operator overloading) to write flexible and dynamic code.
- Use encapsulation (private/public attributes, getters/setters) to protect data integrity.
- Learn abstraction via abstract classes and interfaces to design modular and maintainable systems.
- Trace execution of OOP concepts with visuals and code examples to solve exam problems confidently.
Classes and Objects: The Building Blocks of OOP
Object-Oriented Programming (OOP) is a paradigm that organizes code into objects (instances of classes) that contain data (attributes) and behavior (methods). Python supports OOP natively, making it easier to model real-world entities.
Definitions:
- Class: A blueprint for creating objects. It defines attributes (data) and methods (functions) that the objects will have.
- Object: An instance of a class. Objects are concrete entities created from a class.
- Attribute: Variables that belong to a class or object (e.g.,
name,age). - Method: Functions defined inside a class that operate on the object’s data.
Syntax:
class ClassName:
# Class attributes (shared by all objects)
class_attribute = value
def __init__(self, param1, param2):
# Instance attributes (unique to each object)
self.instance_attribute1 = param1
self.instance_attribute2 = param2
def method_name(self):
# Method definition
return result
Example: Modeling a BankAccount
class BankAccount:
# Class attribute
bank_name = "Global Bank"
def __init__(self, account_holder, balance=0):
self.account_holder = account_holder
self.balance = balance
def deposit(self, amount):
self.balance += amount
return f"Deposited ${amount}. New balance: ${self.balance}"
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds!"
self.balance -= amount
return f"Withdrew ${amount}. New balance: ${self.balance}"
Visualizing Object Creation:
classDiagram
class BankAccount {
-bank_name: str
-account_holder: str
-balance: float
+deposit(amount: float): None
+withdraw(amount: float): None
}
class Customer {
-name: str
-account: BankAccount
+create_account(): None
}
BankAccount "1" o-- "1" Customer : "has"
note for Customer "Customer owns one BankAccount"Worked Example: Creating Objects
# Create two objects of BankAccount
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
# Call methods
print(account1.deposit(200)) # Deposited $200. New balance: $1200
print(account2.withdraw(100)) # Withdrew $100. New balance: $400
State After Each Operation:
| Operation | account1 (Alice) |
account2 (Bob) |
|---|---|---|
| Initialization | balance=1000 |
balance=500 |
account1.deposit(200) |
balance=1200 |
balance=500 |
account2.withdraw(100) |
balance=1200 |
balance=400 |
Inheritance: Reusing and Extending Code
Inheritance allows a child class (subclass) to inherit attributes and methods from a parent class (superclass). This promotes code reusability and hierarchical classification.
Types of Inheritance:
- Single Inheritance: A child class inherits from one parent.
- Multilevel Inheritance: A child class inherits from another child class (grandchild).
- Multiple Inheritance: A child class inherits from multiple parents.
- Hierarchical Inheritance: Multiple child classes inherit from one parent.
Syntax:
class ParentClass:
def parent_method(self):
pass
class ChildClass(ParentClass):
def child_method(self):
pass
Example: Modeling SavingsAccount Inheriting from BankAccount
class SavingsAccount(BankAccount):
def __init__(self, account_holder, balance=0, interest_rate=0.05):
super().__init__(account_holder, balance)
self.interest_rate = interest_rate
def add_interest(self):
interest = self.balance * self.interest_rate
self.balance += interest
return f"Added ${interest:.2f} interest. New balance: ${self.balance:.2f}"
Visualizing Inheritance Hierarchy:
classDiagram
class BankAccount {
+deposit(amount): str
+withdraw(amount): str
}
class SavingsAccount {
+interest_rate: float
+add_interest(): str
}
BankAccount <|-- SavingsAccount : "is-a"Worked Example: Using Inheritance
savings_account = SavingsAccount("Charlie", 2000)
print(savings_account.add_interest()) # Added $100.00 interest. New balance: $2100.00
State After add_interest():
| Attribute | Value Before | Value After |
|---|---|---|
balance |
$2000.00 | $2100.00 |
interest_rate |
0.05 | 0.05 |
Polymorphism: One Interface, Many Forms
Polymorphism allows methods to behave differently based on the object calling them. It includes:
- Method Overriding: Redefining a parent class method in a child class.
- Operator Overloading: Redefining operators (
+,-, etc.) for custom behavior.
Example: Overriding withdraw() in SavingsAccount
class SavingsAccount(BankAccount):
def withdraw(self, amount):
if amount > self.balance * 0.9: # Allow only 90% withdrawal
return "Cannot withdraw more than 90% of balance!"
return super().withdraw(amount)
Worked Example: Polymorphic Behavior
account = SavingsAccount("Dave", 1000)
print(account.withdraw(950)) # Cannot withdraw more than 90% of balance!
Operator Overloading Example: Custom + for BankAccount
class BankAccount:
def __init__(self, balance):
self.balance = balance
def __add__(self, other):
return BankAccount(self.balance + other.balance)
account1 = BankAccount(500)
account2 = BankAccount(300)
combined = account1 + account2
print(combined.balance) # 800
Encapsulation: Hiding Data and Protecting Integrity
Encapsulation restricts direct access to some attributes/methods to prevent unintended interference. Use:
- Private attributes: Prefix with
_(convention) or__(name mangling). - Getters/Setters: Control access to attributes.
Example: Encapsulating balance in BankAccount
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute
def get_balance(self):
return self.__balance
def set_balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
raise ValueError("Balance cannot be negative!")
Worked Example: Using Getters/Setters
account = BankAccount(1000)
print(account.get_balance()) # 1000
account.set_balance(1500)
print(account.get_balance()) # 1500
account.set_balance(-100) # Raises ValueError
Abstraction: Simplifying Complexity
Abstraction hides complex implementation details and exposes only essential features. Use:
- Abstract Classes: Define methods that must be implemented by child classes.
- Interfaces: Pure abstract classes (Python uses
ABCmodule).
Example: Abstract Shape Class
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
Worked Example: Using Abstraction
circle = Circle(5)
print(circle.area()) # 78.5
In the Real World
eSewa (Nepal):
- Uses classes and objects to model users, transactions, and payment gateways. For example, a
Userclass might have attributes likeuser_id,balance, and methods likemake_payment(). - Inheritance is used to extend functionality for premium users (e.g.,
PremiumUserinheriting fromUserwith additional features like priority support).
- Uses classes and objects to model users, transactions, and payment gateways. For example, a
Khalti (Nepal):
- Implements encapsulation to protect sensitive data like transaction IDs and user passwords. Getters/setters ensure controlled access to these attributes.
- Polymorphism is used in payment processing, where different payment methods (e.g.,
CreditCardPayment,MobilePayment) override a commonprocess_payment()method.
Pathao (Nepal):
- Uses object-oriented design to model drivers, riders, and routes. For example, a
Routeclass might have methods likecalculate_distance()andestimate_fare(), while aDriverclass inherits from aUserclass and adds attributes likevehicle_type. - Abstraction simplifies the ride-booking process by hiding complex logic (e.g., GPS routing, fare calculation) behind high-level methods like
book_ride().
- Uses object-oriented design to model drivers, riders, and routes. For example, a
Comparison Table: OOP Concepts
| Concept | Definition | Example Use Case | Advantages |
|---|---|---|---|
| Class | Blueprint for objects | BankAccount, User |
Code organization, reusability |
| Inheritance | Child class inherits from parent | SavingsAccount inherits from BankAccount |
Avoids code duplication, extends functionality |
| Polymorphism | Same method, different behavior | Overriding withdraw() in SavingsAccount |
Flexible and dynamic code |
| Encapsulation | Hiding data with accessors | Private __balance with getters/setters |
Data protection, controlled modifications |
| Abstraction | Hiding complexity | Abstract Shape class |
Simplifies design, focuses on essentials |
Common Pitfalls and Best Practices
Avoid Overusing Inheritance:
- Prefer composition (using objects as attributes) over deep inheritance hierarchies.
- Example: Instead of
SavingsAccountinheriting fromBankAccount, composeSavingsAccountwith aBankAccountobject.
Use Getters/Setters Judiciously:
- Overusing them can lead to verbose code. Only use them when validation or additional logic is needed.
Leverage Python’s Dynamic Nature:
- Python allows modifying classes and objects at runtime, but use this feature sparingly for clarity.
Document Your Classes:
- Use docstrings to explain the purpose, attributes, and methods of your classes.
Exam Tip
Understand the Syntax:
- Memorize keywords like
class,def,self,super(), and__init__. Exams often test syntax correctness.
- Memorize keywords like
Trace Execution:
- For questions involving method calls or inheritance, trace the flow step-by-step. Draw diagrams (like the ones above) to visualize object states.
Practical Applications:
- Expect questions that ask you to model real-world scenarios (e.g., a library system, bank transactions) using OOP principles. Practice designing classes for such scenarios.
Common Exam Questions:
- Define a class with given attributes and methods.
- Extend a class using inheritance and override methods.
- Explain polymorphism with an example of method overriding or operator overloading.
- Implement encapsulation using private attributes and accessors.
- Debug code involving OOP concepts (e.g., incorrect use of
selforsuper()).
Code Tracing:
- For trace questions, create a table showing the state of objects after each operation (like the examples above). This is a high-scoring technique in exams.
Based on the TU BITM syllabus for Programming with Python (IT243), unit 7.
Discussion
Loading…