Table of Contents
Introduction to Object-Oriented Programming in Python
Object-Oriented Programming (OOP) is a programming paradigm that organizes code into classes and objects, making it more modular and reusable.
Classes and Objects
# Define a class
class Dog:
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Constructor method
def __init__(self, name, age, breed):
# Instance attributes (unique to each instance)
self.name = name
self.age = age
self.breed = breed
# Instance method
def bark(self):
return f"{self.name} says Woof!"
def get_info(self):
return f"{self.name} is a {self.age}-year-old {self.breed}"
# Create objects (instances of the class)
dog1 = Dog("Buddy", 3, "Golden Retriever")
dog2 = Dog("Max", 5, "German Shepherd")
# Access attributes and methods
print(dog1.name) # Buddy
print(dog1.bark()) # Buddy says Woof!
print(dog1.get_info()) # Buddy is a 3-year-old Golden Retriever
print(dog2.species) # Canis familiaris (class attribute)
print(dog2.get_info()) # Max is a 5-year-old German Shepherd
Instance vs Class Attributes
class Car:
# Class attributes
wheels = 4
vehicle_type = "automobile"
def __init__(self, make, model, year):
# Instance attributes
self.make = make
self.model = model
self.year = year
self.odometer = 0
def drive(self, miles):
"""Simulate driving the car"""
self.odometer += miles
return f"Drove {miles} miles. Total: {self.odometer} miles"
def get_description(self):
"""Return formatted description"""
return f"{self.year} {self.make} {self.model}"
# Create car instances
car1 = Car("Toyota", "Camry", 2020)
car2 = Car("Honda", "Civic", 2019)
print(car1.get_description()) # 2020 Toyota Camry
print(car1.drive(100)) # Drove 100 miles. Total: 100 miles
print(car1.drive(50)) # Drove 50 miles. Total: 150 miles
# Class attributes are shared
print(car1.wheels) # 4
print(car2.wheels) # 4
# Modify class attribute
Car.wheels = 6
print(car1.wheels) # 6
print(car2.wheels) # 6
Methods and Self
class BankAccount:
def __init__(self, account_holder, initial_balance=0):
self.account_holder = account_holder
self.balance = initial_balance
self.transaction_history = []
def deposit(self, amount):
"""Deposit money to the account"""
if amount > 0:
self.balance += amount
self.transaction_history.append(f"Deposited ${amount}")
return f"Deposited ${amount}. New balance: ${self.balance}"
else:
return "Deposit amount must be positive"
def withdraw(self, amount):
"""Withdraw money from the account"""
if amount > 0:
if amount <= self.balance:
self.balance -= amount
self.transaction_history.append(f"Withdrew ${amount}")
return f"Withdrew ${amount}. New balance: ${self.balance}"
else:
return "Insufficient funds"
else:
return "Withdrawal amount must be positive"
def get_balance(self):
"""Get current balance"""
return f"Current balance: ${self.balance}"
def get_transaction_history(self):
"""Get transaction history"""
if self.transaction_history:
return "
".join(self.transaction_history)
else:
return "No transactions yet"
# Create and use bank account
account = BankAccount("Alice Johnson", 1000)
print(account.get_balance()) # Current balance: $1000
print(account.deposit(500)) # Deposited $500. New balance: $1500
print(account.withdraw(200)) # Withdrew $200. New balance: $1300
print(account.withdraw(2000)) # Insufficient funds
print("
Transaction History:")
print(account.get_transaction_history())
Inheritance
# Parent class (Base class)
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def make_sound(self):
return "Some generic animal sound"
def get_info(self):
return f"{self.name} is a {self.species}"
# Child classes (Derived classes)
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Dog") # Call parent constructor
self.breed = breed
def make_sound(self): # Override parent method
return "Woof!"
def fetch(self): # New method specific to Dog
return f"{self.name} is fetching the ball!"
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name, "Cat")
self.color = color
def make_sound(self): # Override parent method
return "Meow!"
def climb(self): # New method specific to Cat
return f"{self.name} is climbing a tree!"
# Create instances
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")
print(dog.get_info()) # Buddy is a Dog (inherited method)
print(dog.make_sound()) # Woof! (overridden method)
print(dog.fetch()) # Buddy is fetching the ball! (new method)
print(cat.get_info()) # Whiskers is a Cat
print(cat.make_sound()) # Meow!
print(cat.climb()) # Whiskers is climbing a tree!
Encapsulation and Private Attributes
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self._grades = [] # Protected attribute (convention)
self.__gpa = 0.0 # Private attribute (name mangling)
def add_grade(self, grade):
"""Add a grade and recalculate GPA"""
if 0 <= grade <= 100:
self._grades.append(grade)
self.__calculate_gpa()
return f"Grade {grade} added successfully"
else:
return "Grade must be between 0 and 100"
def __calculate_gpa(self): # Private method
"""Calculate GPA based on grades"""
if self._grades:
# Simple GPA calculation (0-4 scale)
average = sum(self._grades) / len(self._grades)
self.__gpa = (average / 100) * 4
def get_gpa(self):
"""Get current GPA"""
return round(self.__gpa, 2)
def get_grades(self):
"""Get copy of grades list"""
return self._grades.copy()
def get_info(self):
"""Get student information"""
return f"Student: {self.name} (ID: {self.student_id}), GPA: {self.get_gpa()}"
# Create student
student = Student("Alice", "S12345")
print(student.add_grade(85)) # Grade 85 added successfully
print(student.add_grade(92)) # Grade 92 added successfully
print(student.add_grade(78)) # Grade 78 added successfully
print(student.get_info()) # Student: Alice (ID: S12345), GPA: 3.42
print(student.get_grades()) # [85, 92, 78]
# Accessing private attributes (not recommended)
# print(student.__gpa) # AttributeError
print(student._Student__gpa) # 3.42 (name mangling - not recommended)
Class Methods and Static Methods
class MathUtils:
pi = 3.14159
def __init__(self, name):
self.name = name
@staticmethod
def add(a, b):
"""Static method - doesn't need class or instance"""
return a + b
@staticmethod
def multiply(a, b):
"""Static method for multiplication"""
return a * b
@classmethod
def circle_area(cls, radius):
"""Class method - has access to class attributes"""
return cls.pi * radius ** 2
@classmethod
def create_calculator(cls, name):
"""Class method as alternative constructor"""
return cls(name)
def instance_method(self):
"""Regular instance method"""
return f"This is {self.name}'s calculator"
# Using static methods (no instance needed)
print(MathUtils.add(5, 3)) # 8
print(MathUtils.multiply(4, 7)) # 28
# Using class methods
print(MathUtils.circle_area(5)) # 78.53975
# Alternative constructor using class method
calc = MathUtils.create_calculator("Scientific Calculator")
print(calc.instance_method()) # This is Scientific Calculator's calculator
# Regular instance creation
calc2 = MathUtils("Basic Calculator")
print(calc2.instance_method()) # This is Basic Calculator's calculator
Special Methods (Magic Methods)
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
"""String representation for users"""
return f"'{self.title}' by {self.author}"
def __repr__(self):
"""String representation for developers"""
return f"Book('{self.title}', '{self.author}', {self.pages})"
def __len__(self):
"""Return length (number of pages)"""
return self.pages
def __eq__(self, other):
"""Check equality"""
if isinstance(other, Book):
return (self.title == other.title and
self.author == other.author)
return False
def __lt__(self, other):
"""Less than comparison (by pages)"""
if isinstance(other, Book):
return self.pages < other.pages
return NotImplemented
def __add__(self, other):
"""Add books (combine pages)"""
if isinstance(other, Book):
combined_title = f"{self.title} & {other.title}"
combined_author = f"{self.author} & {other.author}"
combined_pages = self.pages + other.pages
return Book(combined_title, combined_author, combined_pages)
return NotImplemented
# Create books
book1 = Book("Python Programming", "John Doe", 300)
book2 = Book("Web Development", "Jane Smith", 250)
book3 = Book("Python Programming", "John Doe", 300)
# Using special methods
print(str(book1)) # 'Python Programming' by John Doe
print(repr(book1)) # Book('Python Programming', 'John Doe', 300)
print(len(book1)) # 300
print(book1 == book3) # True
print(book1 == book2) # False
print(book1 < book2) # False (300 < 250)
combined = book1 + book2
print(combined) # 'Python Programming & Web Development' by John Doe & Jane Smith
print(len(combined)) # 550
Practice Exercise
Create a library management system:
from datetime import datetime, timedelta
class Book:
def __init__(self, isbn, title, author, copies=1):
self.isbn = isbn
self.title = title
self.author = author
self.total_copies = copies
self.available_copies = copies
self.borrowed_by = {} # {member_id: borrow_date}
def __str__(self):
return f"'{self.title}' by {self.author} (ISBN: {self.isbn})"
def is_available(self):
return self.available_copies > 0
def borrow(self, member_id):
if self.is_available():
self.available_copies -= 1
self.borrowed_by[member_id] = datetime.now()
return True
return False
def return_book(self, member_id):
if member_id in self.borrowed_by:
self.available_copies += 1
del self.borrowed_by[member_id]
return True
return False
class Member:
def __init__(self, member_id, name, email):
self.member_id = member_id
self.name = name
self.email = email
self.borrowed_books = []
self.join_date = datetime.now()
def __str__(self):
return f"Member: {self.name} (ID: {self.member_id})"
def borrow_book(self, book):
if len(self.borrowed_books) < 5: # Max 5 books
if book.borrow(self.member_id):
self.borrowed_books.append(book.isbn)
return True
return False
def return_book(self, book):
if book.isbn in self.borrowed_books:
if book.return_book(self.member_id):
self.borrowed_books.remove(book.isbn)
return True
return False
class Library:
def __init__(self, name):
self.name = name
self.books = {} # {isbn: Book}
self.members = {} # {member_id: Member}
def add_book(self, isbn, title, author, copies=1):
if isbn in self.books:
self.books[isbn].total_copies += copies
self.books[isbn].available_copies += copies
else:
self.books[isbn] = Book(isbn, title, author, copies)
return f"Added {copies} copy(ies) of '{title}'"
def register_member(self, member_id, name, email):
if member_id not in self.members:
self.members[member_id] = Member(member_id, name, email)
return f"Registered member: {name}"
return "Member ID already exists"
def borrow_book(self, member_id, isbn):
if member_id not in self.members:
return "Member not found"
if isbn not in self.books:
return "Book not found"
member = self.members[member_id]
book = self.books[isbn]
if member.borrow_book(book):
return f"{member.name} borrowed '{book.title}'"
else:
if not book.is_available():
return "Book not available"
else:
return "Member has reached borrowing limit (5 books)"
def return_book(self, member_id, isbn):
if member_id not in self.members:
return "Member not found"
if isbn not in self.books:
return "Book not found"
member = self.members[member_id]
book = self.books[isbn]
if member.return_book(book):
return f"{member.name} returned '{book.title}'"
else:
return "Book was not borrowed by this member"
def search_books(self, query):
results = []
query = query.lower()
for book in self.books.values():
if (query in book.title.lower() or
query in book.author.lower() or
query in book.isbn):
results.append(book)
return results
def get_member_books(self, member_id):
if member_id not in self.members:
return []
member = self.members[member_id]
borrowed_books = []
for isbn in member.borrowed_books:
if isbn in self.books:
borrowed_books.append(self.books[isbn])
return borrowed_books
def get_overdue_books(self, days=14):
overdue = []
cutoff_date = datetime.now() - timedelta(days=days)
for book in self.books.values():
for member_id, borrow_date in book.borrowed_by.items():
if borrow_date < cutoff_date:
member = self.members.get(member_id)
if member:
overdue.append({
'book': book,
'member': member,
'days_overdue': (datetime.now() - borrow_date).days
})
return overdue
# Example usage
def library_demo():
# Create library
library = Library("City Public Library")
# Add books
print(library.add_book("978-0-123456-78-9", "Python Programming", "John Doe", 3))
print(library.add_book("978-0-987654-32-1", "Web Development", "Jane Smith", 2))
print(library.add_book("978-0-555666-77-8", "Data Science", "Bob Johnson", 1))
# Register members
print(library.register_member("M001", "Alice Brown", "alice@email.com"))
print(library.register_member("M002", "Charlie Davis", "charlie@email.com"))
# Borrow books
print(library.borrow_book("M001", "978-0-123456-78-9"))
print(library.borrow_book("M001", "978-0-987654-32-1"))
print(library.borrow_book("M002", "978-0-123456-78-9"))
# Search books
print("
Search results for 'Python':")
results = library.search_books("Python")
for book in results:
status = "Available" if book.is_available() else "Not Available"
print(f" {book} - {status}")
# Check member's borrowed books
print(f"
Alice's borrowed books:")
alice_books = library.get_member_books("M001")
for book in alice_books:
print(f" {book}")
# Return a book
print(library.return_book("M001", "978-0-123456-78-9"))
print(f"
Alice's borrowed books after return:")
alice_books = library.get_member_books("M001")
for book in alice_books:
print(f" {book}")
# Run the demo
library_demo()