Python Intermediate

Python Object-Oriented Programming: Classes and Objects

CodingerWeb
CodingerWeb
27 views 50 min read

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()