Python Classes/Objects

Deep dive into Python classes and objects with advanced concepts and practical examples.

Advanced Class Concepts

Building on the basic OOP concepts, let's explore more advanced features of Python classes.

Class Methods and Static Methods

Instance Methods

Regular methods that operate on instance data:

Example

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):  # Instance method
        return f"Hi, I'm {self.name} and I'm {self.age} years old"
    
    def have_birthday(self):  # Instance method
        self.age += 1
        return f"Happy birthday! Now I'm {self.age}"

person = Person("Alice", 25)
print(person.introduce())
print(person.have_birthday())

Class Methods

Methods that operate on the class itself, not instances:

Example

class Person:
    species = "Homo sapiens"
    population = 0
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.population += 1
    
    @classmethod
    def get_species(cls):
        return cls.species
    
    @classmethod
    def get_population(cls):
        return cls.population
    
    @classmethod
    def from_string(cls, person_str):
        name, age = person_str.split('-')
        return cls(name, int(age))

# Using class methods
print(Person.get_species())  # Homo sapiens
person1 = Person("Alice", 25)
person2 = Person.from_string("Bob-30")
print(Person.get_population())  # 2

Static Methods

Methods that don't operate on instance or class data:

Example

class MathUtils:
    @staticmethod
    def add(x, y):
        return x + y
    
    @staticmethod
    def is_even(number):
        return number % 2 == 0
    
    @staticmethod
    def factorial(n):
        if n <= 1:
            return 1
        return n * MathUtils.factorial(n - 1)

# Using static methods
print(MathUtils.add(5, 3))      # 8
print(MathUtils.is_even(4))     # True
print(MathUtils.factorial(5))   # 120

Special Methods (Magic Methods)

Special methods allow you to define how objects behave with built-in operations:

Example - Comprehensive special methods:

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):
        return f"{self.title} by {self.author}"
    
    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', {self.pages})"
    
    def __len__(self):
        return self.pages
    
    def __eq__(self, other):
        if isinstance(other, Book):
            return self.title == other.title and self.author == other.author
        return False
    
    def __lt__(self, other):
        return self.pages < other.pages
    
    def __add__(self, other):
        if isinstance(other, Book):
            return self.pages + other.pages
        return self.pages + other

# Usage
book1 = Book("1984", "George Orwell", 328)
book2 = Book("Animal Farm", "George Orwell", 112)

print(str(book1))           # 1984 by George Orwell
print(repr(book1))          # Book('1984', 'George Orwell', 328)
print(len(book1))           # 328
print(book1 == book2)       # False
print(book1 > book2)        # True
print(book1 + book2)        # 440

Property Decorators

Properties allow you to access methods like attributes:

Example - Circle class with properties:

import math

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value
    
    @property
    def area(self):
        return math.pi * self._radius ** 2
    
    @property
    def circumference(self):
        return 2 * math.pi * self._radius
    
    @property
    def diameter(self):
        return 2 * self._radius

# Usage
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Area: {circle.area:.2f}")
print(f"Circumference: {circle.circumference:.2f}")

circle.radius = 10
print(f"New area: {circle.area:.2f}")

Class Inheritance

Create new classes based on existing classes:

Example - Vehicle inheritance hierarchy:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.is_running = False
    
    def start(self):
        self.is_running = True
        return f"{self.make} {self.model} started"
    
    def stop(self):
        self.is_running = False
        return f"{self.make} {self.model} stopped"
    
    def info(self):
        return f"{self.year} {self.make} {self.model}"

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors
    
    def honk(self):
        return "Beep beep!"
    
    def info(self):
        return f"{super().info()} with {self.doors} doors"

class Motorcycle(Vehicle):
    def __init__(self, make, model, year, engine_size):
        super().__init__(make, model, year)
        self.engine_size = engine_size
    
    def wheelie(self):
        return "Doing a wheelie!"
    
    def info(self):
        return f"{super().info()} with {self.engine_size}cc engine"

# Usage
car = Car("Toyota", "Camry", 2022, 4)
bike = Motorcycle("Honda", "CBR", 2021, 600)

print(car.info())       # 2022 Toyota Camry with 4 doors
print(car.start())      # Toyota Camry started
print(car.honk())       # Beep beep!

print(bike.info())      # 2021 Honda CBR with 600cc engine
print(bike.wheelie())   # Doing a wheelie!

Multiple Inheritance

Python supports multiple inheritance where a class can inherit from multiple parent classes:

Example

class Flyable:
    def fly(self):
        return "Flying through the air"

class Swimmable:
    def swim(self):
        return "Swimming in water"

class Duck(Flyable, Swimmable):
    def __init__(self, name):
        self.name = name
    
    def quack(self):
        return f"{self.name} says quack!"

class Penguin(Swimmable):
    def __init__(self, name):
        self.name = name
    
    def slide(self):
        return f"{self.name} is sliding on ice"

# Usage
duck = Duck("Donald")
penguin = Penguin("Pingu")

print(duck.quack())     # Donald says quack!
print(duck.fly())       # Flying through the air
print(duck.swim())      # Swimming in water

print(penguin.swim())   # Swimming in water
print(penguin.slide())  # Pingu is sliding on ice

Abstract Base Classes

Define interfaces that subclasses must implement:

Example

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
    
    def description(self):
        return f"This is a {self.__class__.__name__} with area {self.area()}"

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

# Usage
rectangle = Rectangle(5, 3)
circle = Circle(4)

print(rectangle.description())
print(f"Rectangle perimeter: {rectangle.perimeter()}")
print(circle.description())
print(f"Circle perimeter: {circle.perimeter():.2f}")

Composition vs Inheritance

Sometimes composition is better than inheritance:

Example - Composition approach:

class Engine:
    def __init__(self, horsepower, fuel_type):
        self.horsepower = horsepower
        self.fuel_type = fuel_type
        self.is_running = False
    
    def start(self):
        self.is_running = True
        return f"{self.horsepower}HP {self.fuel_type} engine started"
    
    def stop(self):
        self.is_running = False
        return f"Engine stopped"

class GPS:
    def __init__(self):
        self.current_location = "Unknown"
    
    def navigate_to(self, destination):
        return f"Navigating to {destination}"

class Car:
    def __init__(self, make, model, engine, has_gps=False):
        self.make = make
        self.model = model
        self.engine = engine  # Composition
        self.gps = GPS() if has_gps else None  # Composition
    
    def start(self):
        return self.engine.start()
    
    def navigate(self, destination):
        if self.gps:
            return self.gps.navigate_to(destination)
        return "No GPS available"

# Usage
engine = Engine(300, "Gasoline")
car = Car("BMW", "M3", engine, has_gps=True)

print(car.start())                    # 300HP Gasoline engine started
print(car.navigate("New York"))       # Navigating to New York

Practical Example: Bank Account System

Complete banking system example:

from datetime import datetime
from abc import ABC, abstractmethod

class Transaction:
    def __init__(self, transaction_type, amount, description=""):
        self.timestamp = datetime.now()
        self.type = transaction_type
        self.amount = amount
        self.description = description
    
    def __str__(self):
        return f"{self.timestamp.strftime('%Y-%m-%d %H:%M')} - {self.type}: ${self.amount:.2f} - {self.description}"

class Account(ABC):
    def __init__(self, account_number, owner_name, initial_balance=0):
        self.account_number = account_number
        self.owner_name = owner_name
        self._balance = initial_balance
        self.transactions = []
        self._add_transaction("OPENING", initial_balance, "Account opened")
    
    @property
    def balance(self):
        return self._balance
    
    def _add_transaction(self, transaction_type, amount, description=""):
        transaction = Transaction(transaction_type, amount, description)
        self.transactions.append(transaction)
    
    def deposit(self, amount, description="Deposit"):
        if amount > 0:
            self._balance += amount
            self._add_transaction("DEPOSIT", amount, description)
            return True
        return False
    
    @abstractmethod
    def withdraw(self, amount, description="Withdrawal"):
        pass
    
    def get_statement(self):
        statement = f"\n--- Account Statement for {self.owner_name} ---\n"
        statement += f"Account Number: {self.account_number}\n"
        statement += f"Current Balance: ${self.balance:.2f}\n\n"
        statement += "Recent Transactions:\n"
        for transaction in self.transactions[-10:]:  # Last 10 transactions
            statement += str(transaction) + "\n"
        return statement

class SavingsAccount(Account):
    def __init__(self, account_number, owner_name, initial_balance=0, interest_rate=0.02):
        super().__init__(account_number, owner_name, initial_balance)
        self.interest_rate = interest_rate
    
    def withdraw(self, amount, description="Withdrawal"):
        if 0 < amount <= self._balance:
            self._balance -= amount
            self._add_transaction("WITHDRAWAL", amount, description)
            return True
        return False
    
    def add_interest(self):
        interest = self._balance * self.interest_rate / 12  # Monthly interest
        self.deposit(interest, "Monthly interest")

class CheckingAccount(Account):
    def __init__(self, account_number, owner_name, initial_balance=0, overdraft_limit=100):
        super().__init__(account_number, owner_name, initial_balance)
        self.overdraft_limit = overdraft_limit
    
    def withdraw(self, amount, description="Withdrawal"):
        if 0 < amount <= (self._balance + self.overdraft_limit):
            self._balance -= amount
            self._add_transaction("WITHDRAWAL", amount, description)
            if self._balance < 0:
                self._add_transaction("OVERDRAFT FEE", 35, "Overdraft fee")
                self._balance -= 35
            return True
        return False

# Usage
savings = SavingsAccount("SAV001", "Alice Johnson", 1000, 0.03)
checking = CheckingAccount("CHK001", "Bob Smith", 500, 200)

# Perform transactions
savings.deposit(500, "Salary deposit")
savings.withdraw(200, "ATM withdrawal")
savings.add_interest()

checking.deposit(300, "Cash deposit")
checking.withdraw(900, "Rent payment")  # This will trigger overdraft

print(savings.get_statement())
print(checking.get_statement())