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