Chapter 5: Object-Oriented Programming - Master Classes, Objects, and Inheritance
Welcome to the comprehensive guide to Python Object-Oriented Programming (OOP)! This chapter covers classes, objects, inheritance, polymorphism, and encapsulation - the fundamental concepts that make Python a powerful object-oriented language. Understanding OOP is crucial for building complex, maintainable applications.
Learning Objectives
By the end of this chapter, you will understand:
- Classes and objects - the foundation of OOP
- Attributes and methods - data and behavior in classes
- Constructors and destructors - object lifecycle management
- Inheritance - creating class hierarchies
- Polymorphism - different behaviors for different objects
- Encapsulation - data hiding and access control
- Special methods - magic methods for custom behavior
- Class methods and static methods - alternative method types
- Property decorators - controlled attribute access
- Best practices for OOP design in Python
Classes and Objects
Basic Class Definition
A class is a blueprint for creating objects:
# Basic class definition
class Person:
"""A simple Person class."""
def __init__(self, name, age):
"""Initialize a Person object."""
self.name = name
self.age = age
def introduce(self):
"""Introduce the person."""
return f"Hi, I'm {self.name} and I'm {self.age} years old."
def have_birthday(self):
"""Increase age by 1."""
self.age += 1
return f"Happy birthday! {self.name} is now {self.age} years old."
# Creating objects (instances)
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)
# Using objects
print(person1.introduce()) # Hi, I'm Alice and I'm 25 years old.
print(person2.introduce()) # Hi, I'm Bob and I'm 30 years old.
print(person1.have_birthday()) # Happy birthday! Alice is now 26 years old.
Class Attributes vs Instance Attributes
class Student:
"""Student class with class and instance attributes."""
# Class attribute (shared by all instances)
school_name = "Python University"
total_students = 0
def __init__(self, name, student_id, major):
"""Initialize a Student object."""
# Instance attributes (unique to each instance)
self.name = name
self.student_id = student_id
self.major = major
self.grades = []
# Increment class attribute
Student.total_students += 1
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate average grade."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
@classmethod
def get_total_students(cls):
"""Get total number of students."""
return cls.total_students
# Creating students
student1 = Student("Alice", "S001", "Computer Science")
student2 = Student("Bob", "S002", "Mathematics")
# Accessing class attributes
print(f"School: {Student.school_name}")
print(f"Total students: {Student.get_total_students()}")
# Accessing instance attributes
student1.add_grade(85)
student1.add_grade(92)
student1.add_grade(78)
print(f"{student1.name}'s average: {student1.get_average():.2f}")
Methods in Classes
Instance Methods
class BankAccount:
"""Bank account class with instance methods."""
def __init__(self, account_holder, initial_balance=0):
"""Initialize a bank account."""
self.account_holder = account_holder
self.balance = initial_balance
self.transaction_history = []
def deposit(self, amount):
"""Deposit money into the account."""
if amount > 0:
self.balance += amount
self.transaction_history.append(f"Deposit: +${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"Withdrawal: -${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 self.balance
def get_transaction_history(self):
"""Get transaction history."""
return self.transaction_history.copy()
# Using the class
account = BankAccount("Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
print(f"Current balance: ${account.get_balance()}")
print("Transaction history:", account.get_transaction_history())
Class Methods
class Date:
"""Date class with class methods."""
def __init__(self, year, month, day):
"""Initialize a Date object."""
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""Create Date object from string (YYYY-MM-DD)."""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day)
@classmethod
def today(cls):
"""Create Date object for today."""
import datetime
today = datetime.date.today()
return cls(today.year, today.month, today.day)
def __str__(self):
"""String representation of the date."""
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# Using class methods
date1 = Date.from_string("2024-01-15")
date2 = Date.today()
print(f"Date from string: {date1}")
print(f"Today's date: {date2}")
Static Methods
class MathUtils:
"""Math utilities class with static methods."""
@staticmethod
def add(a, b):
"""Add two numbers."""
return a + b
@staticmethod
def multiply(a, b):
"""Multiply two numbers."""
return a * b
@staticmethod
def is_prime(n):
"""Check if a number is prime."""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@staticmethod
def factorial(n):
"""Calculate factorial of a number."""
if n < 0:
raise ValueError("Factorial is not defined for negative numbers")
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)
# Using static methods
print(f"5 + 3 = {MathUtils.add(5, 3)}")
print(f"4 * 7 = {MathUtils.multiply(4, 7)}")
print(f"Is 17 prime? {MathUtils.is_prime(17)}")
print(f"5! = {MathUtils.factorial(5)}")
Inheritance
Basic Inheritance
# Parent class
class Animal:
"""Base Animal class."""
def __init__(self, name, species):
"""Initialize an Animal."""
self.name = name
self.species = species
self.energy = 100
def eat(self, food):
"""Animal eats food."""
self.energy += 10
return f"{self.name} ate {food} and gained energy"
def sleep(self):
"""Animal sleeps."""
self.energy += 20
return f"{self.name} slept and restored energy"
def make_sound(self):
"""Make a generic animal sound."""
return f"{self.name} makes a sound"
# Child class
class Dog(Animal):
"""Dog class inheriting from Animal."""
def __init__(self, name, breed):
"""Initialize a Dog."""
super().__init__(name, "Canine") # Call parent constructor
self.breed = breed
def make_sound(self):
"""Override parent method."""
return f"{self.name} barks: Woof! Woof!"
def fetch(self, item):
"""Dog-specific method."""
self.energy -= 5
return f"{self.name} fetched the {item}"
# Another child class
class Cat(Animal):
"""Cat class inheriting from Animal."""
def __init__(self, name, color):
"""Initialize a Cat."""
super().__init__(name, "Feline")
self.color = color
def make_sound(self):
"""Override parent method."""
return f"{self.name} meows: Meow! Meow!"
def climb(self, height):
"""Cat-specific method."""
self.energy -= 3
return f"{self.name} climbed {height} feet"
# Using inheritance
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")
print(dog.make_sound()) # Buddy barks: Woof! Woof!
print(cat.make_sound()) # Whiskers meows: Meow! Meow!
print(dog.fetch("ball"))
print(cat.climb(5))
print(f"Dog energy: {dog.energy}")
print(f"Cat energy: {cat.energy}")
Multiple Inheritance
# Multiple parent classes
class Flyable:
"""Mixin class for flying ability."""
def fly(self):
"""Fly method."""
return f"{self.name} is flying"
class Swimmable:
"""Mixin class for swimming ability."""
def swim(self):
"""Swim method."""
return f"{self.name} is swimming"
class Duck(Animal, Flyable, Swimmable):
"""Duck class with multiple inheritance."""
def __init__(self, name):
"""Initialize a Duck."""
Animal.__init__(self, name, "Bird")
def make_sound(self):
"""Override parent method."""
return f"{self.name} quacks: Quack! Quack!"
# Using multiple inheritance
duck = Duck("Donald")
print(duck.make_sound()) # Donald quacks: Quack! Quack!
print(duck.fly()) # Donald is flying
print(duck.swim()) # Donald is swimming
Encapsulation
Private and Protected Attributes
class BankAccount:
"""Bank account with encapsulation."""
def __init__(self, account_holder, initial_balance=0):
"""Initialize a bank account."""
self.account_holder = account_holder
self._balance = initial_balance # Protected attribute
self.__account_number = self._generate_account_number() # Private attribute
def _generate_account_number(self):
"""Generate a private account number."""
import random
return f"ACC{random.randint(100000, 999999)}"
def get_balance(self):
"""Public method to get balance."""
return self._balance
def deposit(self, amount):
"""Public method to deposit money."""
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount):
"""Public method to withdraw money."""
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
def get_account_info(self):
"""Get account information (excluding private data)."""
return {
"holder": self.account_holder,
"balance": self._balance
}
# Using encapsulation
account = BankAccount("Alice", 1000)
# Accessing public methods
print(f"Balance: ${account.get_balance()}")
account.deposit(500)
print(f"Balance after deposit: ${account.get_balance()}")
# Protected attribute (accessible but not recommended)
print(f"Protected balance: {account._balance}")
# Private attribute (not directly accessible)
# print(account.__account_number) # This would cause an error
Property Decorators
class Temperature:
"""Temperature class with property decorators."""
def __init__(self, celsius=0):
"""Initialize temperature in Celsius."""
self._celsius = celsius
@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius."""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Get temperature in Fahrenheit."""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature in Fahrenheit."""
self.celsius = (value - 32) * 5/9
@property
def kelvin(self):
"""Get temperature in Kelvin."""
return self._celsius + 273.15
@kelvin.setter
def kelvin(self, value):
"""Set temperature in Kelvin."""
if value < 0:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value - 273.15
# Using properties
temp = Temperature(25)
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")
print(f"Kelvin: {temp.kelvin}K")
# Setting temperature using different units
temp.fahrenheit = 86
print(f"After setting 86°F: {temp.celsius}°C")
temp.kelvin = 300
print(f"After setting 300K: {temp.celsius}°C")
Special Methods (Magic Methods)
Common Special Methods
class Vector:
"""Vector class with special methods."""
def __init__(self, x, y):
"""Initialize a vector."""
self.x = x
self.y = y
def __str__(self):
"""String representation for users."""
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""String representation for developers."""
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
"""Add two vectors."""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other):
"""Subtract two vectors."""
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar):
"""Multiply vector by scalar."""
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __eq__(self, other):
"""Check if two vectors are equal."""
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False
def __len__(self):
"""Return the magnitude of the vector."""
return int((self.x**2 + self.y**2)**0.5)
def __bool__(self):
"""Return True if vector is not zero."""
return self.x != 0 or self.y != 0
# Using special methods
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1: {v1}") # Vector(3, 4)
print(f"v2: {v2}") # Vector(1, 2)
v3 = v1 + v2
print(f"v1 + v2: {v3}") # Vector(4, 6)
v4 = v1 * 2
print(f"v1 * 2: {v4}") # Vector(6, 8)
print(f"v1 == v2: {v1 == v2}") # False
print(f"Length of v1: {len(v1)}") # 5
print(f"v1 is truthy: {bool(v1)}") # True
Context Manager Methods
class FileManager:
"""File manager with context manager methods."""
def __init__(self, filename, mode):
"""Initialize file manager."""
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""Enter the context manager."""
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit the context manager."""
if self.file:
self.file.close()
if exc_type:
print(f"Exception occurred: {exc_val}")
return False # Don't suppress exceptions
# Using context manager
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")
# File is automatically closed
Polymorphism
Method Overriding
class Shape:
"""Base Shape class."""
def area(self):
"""Calculate area (to be overridden)."""
raise NotImplementedError("Subclass must implement area method")
def perimeter(self):
"""Calculate perimeter (to be overridden)."""
raise NotImplementedError("Subclass must implement perimeter method")
class Rectangle(Shape):
"""Rectangle class."""
def __init__(self, width, height):
"""Initialize rectangle."""
self.width = width
self.height = height
def area(self):
"""Calculate rectangle area."""
return self.width * self.height
def perimeter(self):
"""Calculate rectangle perimeter."""
return 2 * (self.width + self.height)
class Circle(Shape):
"""Circle class."""
def __init__(self, radius):
"""Initialize circle."""
self.radius = radius
def area(self):
"""Calculate circle area."""
import math
return math.pi * self.radius ** 2
def perimeter(self):
"""Calculate circle perimeter (circumference)."""
import math
return 2 * math.pi * self.radius
# Polymorphism in action
shapes = [
Rectangle(5, 3),
Circle(4),
Rectangle(2, 8)
]
for shape in shapes:
print(f"{type(shape).__name__}: Area = {shape.area():.2f}, Perimeter = {shape.perimeter():.2f}")
Duck Typing
class Duck:
"""Duck class."""
def quack(self):
return "Quack!"
def fly(self):
return "Flying with wings"
class Airplane:
"""Airplane class."""
def quack(self):
return "I'm not a duck, but I can make noise"
def fly(self):
return "Flying with engines"
def make_it_quack_and_fly(thing):
"""Function that works with any object that has quack and fly methods."""
print(f"Quack: {thing.quack()}")
print(f"Fly: {thing.fly()}")
# Duck typing - objects are used based on their methods, not their type
duck = Duck()
airplane = Airplane()
make_it_quack_and_fly(duck)
make_it_quack_and_fly(airplane)
Advanced OOP Concepts
Abstract Base Classes
from abc import ABC, abstractmethod
class Animal(ABC):
"""Abstract base class for animals."""
def __init__(self, name):
"""Initialize animal."""
self.name = name
@abstractmethod
def make_sound(self):
"""Abstract method - must be implemented by subclasses."""
pass
def sleep(self):
"""Concrete method - can be used by all subclasses."""
return f"{self.name} is sleeping"
class Dog(Animal):
"""Dog class implementing Animal."""
def make_sound(self):
"""Implement abstract method."""
return f"{self.name} barks"
class Cat(Animal):
"""Cat class implementing Animal."""
def make_sound(self):
"""Implement abstract method."""
return f"{self.name} meows"
# Using abstract base classes
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.make_sound())
print(cat.make_sound())
print(dog.sleep())
Composition vs Inheritance
# Composition example
class Engine:
"""Engine class."""
def __init__(self, horsepower):
"""Initialize engine."""
self.horsepower = horsepower
def start(self):
"""Start the engine."""
return "Engine started"
def stop(self):
"""Stop the engine."""
return "Engine stopped"
class Car:
"""Car class using composition."""
def __init__(self, make, model, engine_horsepower):
"""Initialize car."""
self.make = make
self.model = model
self.engine = Engine(engine_horsepower) # Composition
def start(self):
"""Start the car."""
return f"{self.make} {self.model}: {self.engine.start()}"
def stop(self):
"""Stop the car."""
return f"{self.make} {self.model}: {self.engine.stop()}"
# Using composition
car = Car("Toyota", "Camry", 200)
print(car.start())
print(car.stop())
Practical Examples
Example 1: Library Management System
class Book:
"""Book class."""
def __init__(self, title, author, isbn, copies=1):
"""Initialize a book."""
self.title = title
self.author = author
self.isbn = isbn
self.copies = copies
self.available_copies = copies
def borrow(self):
"""Borrow a copy of the book."""
if self.available_copies > 0:
self.available_copies -= 1
return True
return False
def return_book(self):
"""Return a copy of the book."""
if self.available_copies < self.copies:
self.available_copies += 1
return True
return False
def is_available(self):
"""Check if book is available."""
return self.available_copies > 0
def __str__(self):
"""String representation."""
return f"{self.title} by {self.author} (ISBN: {self.isbn})"
class Member:
"""Library member class."""
def __init__(self, name, member_id):
"""Initialize a member."""
self.name = name
self.member_id = member_id
self.borrowed_books = []
def borrow_book(self, book):
"""Borrow a book."""
if book.borrow():
self.borrowed_books.append(book)
return f"Successfully borrowed {book.title}"
return f"Sorry, {book.title} is not available"
def return_book(self, book):
"""Return a book."""
if book in self.borrowed_books:
book.return_book()
self.borrowed_books.remove(book)
return f"Successfully returned {book.title}"
return f"You haven't borrowed {book.title}"
def get_borrowed_books(self):
"""Get list of borrowed books."""
return [book.title for book in self.borrowed_books]
class Library:
"""Library class."""
def __init__(self, name):
"""Initialize library."""
self.name = name
self.books = []
self.members = []
def add_book(self, book):
"""Add a book to the library."""
self.books.append(book)
return f"Added {book.title} to the library"
def add_member(self, member):
"""Add a member to the library."""
self.members.append(member)
return f"Added {member.name} as a member"
def search_books(self, query):
"""Search for books by title or author."""
results = []
query_lower = query.lower()
for book in self.books:
if (query_lower in book.title.lower() or
query_lower in book.author.lower()):
results.append(book)
return results
def get_available_books(self):
"""Get all available books."""
return [book for book in self.books if book.is_available()]
# Using the library system
library = Library("Central Library")
# Add books
book1 = Book("Python Programming", "John Doe", "123456789", 3)
book2 = Book("Data Structures", "Jane Smith", "987654321", 2)
library.add_book(book1)
library.add_book(book2)
# Add members
member1 = Member("Alice", "M001")
member2 = Member("Bob", "M002")
library.add_member(member1)
library.add_member(member2)
# Borrow books
print(member1.borrow_book(book1))
print(member1.borrow_book(book2))
print(member2.borrow_book(book1))
# Search books
results = library.search_books("Python")
for book in results:
print(f"Found: {book}")
# Check available books
available = library.get_available_books()
print(f"Available books: {len(available)}")
Example 2: Employee Management System
class Employee:
"""Base Employee class."""
def __init__(self, name, employee_id, base_salary):
"""Initialize employee."""
self.name = name
self.employee_id = employee_id
self.base_salary = base_salary
def calculate_salary(self):
"""Calculate salary (to be overridden)."""
return self.base_salary
def get_info(self):
"""Get employee information."""
return f"ID: {self.employee_id}, Name: {self.name}, Salary: ${self.calculate_salary():.2f}"
class Manager(Employee):
"""Manager class."""
def __init__(self, name, employee_id, base_salary, bonus=0):
"""Initialize manager."""
super().__init__(name, employee_id, base_salary)
self.bonus = bonus
self.team = []
def calculate_salary(self):
"""Calculate manager salary with bonus."""
return self.base_salary + self.bonus
def add_team_member(self, employee):
"""Add team member."""
self.team.append(employee)
return f"Added {employee.name} to {self.name}'s team"
def get_team_size(self):
"""Get team size."""
return len(self.team)
class Developer(Employee):
"""Developer class."""
def __init__(self, name, employee_id, base_salary, programming_language):
"""Initialize developer."""
super().__init__(name, employee_id, base_salary)
self.programming_language = programming_language
self.projects = []
def add_project(self, project):
"""Add project."""
self.projects.append(project)
return f"Added {project} to {self.name}'s projects"
def get_info(self):
"""Get developer information."""
base_info = super().get_info()
return f"{base_info}, Language: {self.programming_language}"
class Salesperson(Employee):
"""Salesperson class."""
def __init__(self, name, employee_id, base_salary, commission_rate=0.05):
"""Initialize salesperson."""
super().__init__(name, employee_id, base_salary)
self.commission_rate = commission_rate
self.sales = 0
def add_sale(self, amount):
"""Add a sale."""
self.sales += amount
return f"Added ${amount} sale for {self.name}"
def calculate_salary(self):
"""Calculate salesperson salary with commission."""
commission = self.sales * self.commission_rate
return self.base_salary + commission
# Using the employee system
employees = [
Manager("Alice Johnson", "M001", 80000, 10000),
Developer("Bob Smith", "D001", 70000, "Python"),
Developer("Charlie Brown", "D002", 75000, "JavaScript"),
Salesperson("Diana Prince", "S001", 50000, 0.08)
]
# Manager operations
manager = employees[0]
manager.add_team_member(employees[1])
manager.add_team_member(employees[2])
print(f"Team size: {manager.get_team_size()}")
# Developer operations
developer = employees[1]
developer.add_project("Web Application")
developer.add_project("Mobile App")
# Salesperson operations
salesperson = employees[3]
salesperson.add_sale(10000)
salesperson.add_sale(15000)
# Display all employees
for employee in employees:
print(employee.get_info())
Best Practices for OOP
Design Principles
# Single Responsibility Principle
class User:
"""User class with single responsibility."""
def __init__(self, name, email):
self.name = name
self.email = email
class EmailValidator:
"""Email validation with single responsibility."""
@staticmethod
def is_valid(email):
return "@" in email and "." in email
# Open/Closed Principle
class Shape:
"""Open for extension, closed for modification."""
def area(self):
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
# Dependency Inversion Principle
class Database:
"""Abstract database interface."""
def save(self, data):
raise NotImplementedError
class MySQLDatabase(Database):
def save(self, data):
return f"Saved to MySQL: {data}"
class UserService:
"""User service depending on abstraction."""
def __init__(self, database):
self.database = database
def create_user(self, user):
return self.database.save(user.name)
# Using dependency injection
mysql_db = MySQLDatabase()
user_service = UserService(mysql_db)
user = User("Alice", "[email protected]")
result = user_service.create_user(user)
print(result)
Summary
In this chapter, we've covered:
- Classes and objects: The foundation of OOP in Python
- Methods: Instance, class, and static methods
- Inheritance: Single and multiple inheritance patterns
- Encapsulation: Data hiding and property decorators
- Special methods: Magic methods for custom behavior
- Polymorphism: Method overriding and duck typing
- Advanced concepts: Abstract base classes and composition
- Design principles: SOLID principles and best practices
Object-Oriented Programming is a powerful paradigm that helps you create maintainable, reusable, and organized code. Understanding these concepts is essential for building complex applications and working with Python frameworks and libraries.
Next Steps
Now that you understand Object-Oriented Programming, you're ready to explore:
- Advanced Python Topics: Decorators, generators, and context managers
- File Handling: Reading from and writing to files
- Error Handling: Exception management and debugging
- Testing: Unit testing and test-driven development
- Package Management: Virtual environments and package distribution
Ready to explore advanced Python features? Continue with Chapter 6: Advanced Python Topics to master Python's advanced concepts!