Module 10 - Object-Oriented Programming Basics
This module covers Object-Oriented Programming (OOP) basics in Python. You'll learn how to create classes, objects, attributes, and methods—the foundation of OOP in Python.
1. What is Object-Oriented Programming?
1.1 OOP Concepts
OOP is a programming paradigm that organizes code around objects (data + behavior) rather than functions and logic.
Key Principles:
- Encapsulation: Bundle data and methods together
- Abstraction: Hide complex implementation details
- Inheritance: Create new classes from existing ones
- Polymorphism: Same interface, different implementations
OOP makes code more modular, reusable, and easier to maintain. It models real-world entities naturally.
2. Classes and Objects
2.1 Creating a Class
# Define a class
class Dog:
pass
# Create an object (instance)
my_dog = Dog()
print(type(my_dog)) # <class '__main__.Dog'>
2.2 Adding Attributes
# Class with attributes
class Dog:
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age
# Create instances
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
print(dog1.name) # Buddy
print(dog2.age) # 5
2.3 The init Method
class Person:
def __init__(self, name, age):
"""Constructor method"""
self.name = name
self.age = age
print(f"Person created: {name}")
person = Person("Alice", 25)
# Output: Person created: Alice
self refers to the instance itself. It's always the first parameter of instance methods, but you don't pass it explicitly when calling methods.
3. Instance Methods
3.1 Defining Methods
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
return f"{self.name} says Woof!"
def get_age_in_human_years(self):
return self.age * 7
dog = Dog("Buddy", 3)
print(dog.bark()) # Buddy says Woof!
print(dog.get_age_in_human_years()) # 21
3.2 Methods with Parameters
class Calculator:
def __init__(self):
self.result = 0
def add(self, value):
self.result += value
return self
def subtract(self, value):
self.result -= value
return self
def get_result(self):
return self.result
calc = Calculator()
result = calc.add(10).subtract(3).get_result()
print(result) # 7
4. Instance vs Class Attributes
4.1 Instance Attributes
class Student:
def __init__(self, name, grade):
self.name = name # Instance attribute
self.grade = grade # Instance attribute
student1 = Student("Alice", "A")
student2 = Student("Bob", "B")
print(student1.name) # Alice
print(student2.name) # Bob
4.2 Class Attributes
class Dog:
# Class attribute (shared by all instances)
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
print(dog1.species) # Canis familiaris
print(dog2.species) # Canis familiaris
print(Dog.species) # Canis familiaris
# Modifying class attribute
Dog.species = "Modified species"
print(dog1.species) # Modified species
print(dog2.species) # Modified species
4.3 Instance vs Class Comparison
class Counter:
count = 0 # Class attribute
def __init__(self, name):
self.name = name # Instance attribute
Counter.count += 1 # Increment class attribute
self.instance_id = Counter.count # Instance-specific ID
c1 = Counter("First")
c2 = Counter("Second")
c3 = Counter("Third")
print(Counter.count) # 3
print(c1.instance_id) # 1
print(c2.instance_id) # 2
print(c3.instance_id) # 3
5. Class Methods and Static Methods
5.1 Class Methods
class Employee:
raise_amount = 1.05 # Class attribute
def __init__(self, name, salary):
self.name = name
self.salary = salary
@classmethod
def set_raise_amount(cls, amount):
"""Modify class attribute"""
cls.raise_amount = amount
@classmethod
def from_string(cls, emp_str):
"""Alternative constructor"""
name, salary = emp_str.split('-')
return cls(name, int(salary))
def apply_raise(self):
self.salary = int(self.salary * self.raise_amount)
# Using class method
Employee.set_raise_amount(1.10)
# Alternative constructor
emp1 = Employee.from_string("Alice-50000")
print(emp1.name, emp1.salary) # Alice 50000
5.2 Static Methods
class MathOperations:
@staticmethod
def add(a, b):
"""Static method - doesn't access instance or class"""
return a + b
@staticmethod
def is_even(number):
return number % 2 == 0
# Call without creating instance
print(MathOperations.add(5, 3)) # 8
print(MathOperations.is_even(10)) # True
5.3 When to Use Each
| Method Type | Access | Use Case |
|---|---|---|
| Instance method | self (instance) | Operations on instance data |
| Class method | cls (class) | Factory methods, modify class state |
| Static method | Neither | Utility functions related to class |
6. Properties and Encapsulation
6.1 Public vs Private Attributes
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public
self._balance = balance # Protected (convention)
self.__pin = 1234 # Private (name mangling)
def get_balance(self):
return self._balance
def deposit(self, amount):
if amount > 0:
self._balance += amount
account = BankAccount("Alice", 1000)
print(account.owner) # Alice (public)
print(account._balance) # 1000 (accessible but discouraged)
# print(account.__pin) # AttributeError
print(account._BankAccount__pin) # 1234 (name mangling workaround)
- Single underscore
_attr: Protected (internal use) - Double underscore
__attr: Private (name mangling) - No underscore
attr: Public
These are conventions, not strict access control!
6.2 Property Decorator
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Getter"""
return self._radius
@radius.setter
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""Computed property"""
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(circle.radius) # 5 (calls getter)
print(circle.area) # 78.53975
circle.radius = 10 # Calls setter
print(circle.area) # 314.159
# circle.radius = -5 # ValueError
6.3 Getters and Setters
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 32
print(temp.celsius) # 0.0
7. String Representation
7.1 str and repr
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
"""User-friendly string"""
return f"{self.title} by {self.author} ({self.year})"
def __repr__(self):
"""Developer-friendly representation"""
return f"Book('{self.title}', '{self.author}', {self.year})"
book = Book("1984", "George Orwell", 1949)
print(str(book)) # 1984 by George Orwell (1949)
print(repr(book)) # Book('1984', 'George Orwell', 1949)
print(book) # Uses __str__
__str__: Human-readable, for end users__repr__: Unambiguous, for developers/debugging- If only
__repr__is defined, it's used for both
8. Object Comparison
8.1 Equality Methods
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
"""Equal to"""
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False
def __ne__(self, other):
"""Not equal to"""
return not self.__eq__(other)
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(2, 3)
print(p1 == p2) # True
print(p1 == p3) # False
print(p1 != p3) # True
8.2 Ordering Methods
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __lt__(self, other):
"""Less than"""
return self.grade < other.grade
def __le__(self, other):
"""Less than or equal"""
return self.grade <= other.grade
def __gt__(self, other):
"""Greater than"""
return self.grade > other.grade
def __ge__(self, other):
"""Greater than or equal"""
return self.grade >= other.grade
students = [
Student("Alice", 85),
Student("Bob", 92),
Student("Charlie", 78)
]
sorted_students = sorted(students)
for s in sorted_students:
print(s.name, s.grade)
# Charlie 78
# Alice 85
# Bob 92
9. Real-World Examples
9.1 Bank Account Class
class BankAccount:
"""A simple bank account implementation."""
interest_rate = 0.02 # Class attribute
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
self._transactions = []
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount > 0:
self._balance += amount
self._transactions.append(f"Deposit: +${amount}")
return True
return False
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
self._transactions.append(f"Withdraw: -${amount}")
return True
return False
def apply_interest(self):
interest = self._balance * self.interest_rate
self._balance += interest
self._transactions.append(f"Interest: +${interest:.2f}")
def get_statement(self):
statement = f"Account: {self.owner}\n"
statement += f"Balance: ${self._balance:.2f}\n"
statement += "Transactions:\n"
for transaction in self._transactions:
statement += f" {transaction}\n"
return statement
def __str__(self):
return f"BankAccount({self.owner}, ${self._balance:.2f})"
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
account.apply_interest()
print(account.get_statement())
9.2 Shopping Cart Class
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name}: ${self.price:.2f}"
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, product, quantity=1):
self.items.append({"product": product, "quantity": quantity})
def remove_item(self, product_name):
self.items = [item for item in self.items
if item["product"].name != product_name]
def get_total(self):
return sum(item["product"].price * item["quantity"]
for item in self.items)
def display_cart(self):
print("Shopping Cart:")
for item in self.items:
product = item["product"]
quantity = item["quantity"]
subtotal = product.price * quantity
print(f" {product.name} x{quantity} = ${subtotal:.2f}")
print(f"Total: ${self.get_total():.2f}")
# Usage
cart = ShoppingCart()
cart.add_item(Product("Apple", 0.50), 5)
cart.add_item(Product("Bread", 2.50), 2)
cart.add_item(Product("Milk", 3.99), 1)
cart.display_cart()
10. Best Practices
10.1 Class Design Principles
# ✅ Single Responsibility
class User:
"""Handles user data only"""
def __init__(self, username, email):
self.username = username
self.email = email
class UserRepository:
"""Handles user persistence"""
def save(self, user):
pass
def find_by_username(self, username):
pass
# ❌ Too many responsibilities
class User:
def __init__(self, username):
self.username = username
def save_to_database(self): # Database logic
pass
def send_email(self): # Email logic
pass
def generate_report(self): # Reporting logic
pass
10.2 Naming Conventions
# Class names: PascalCase
class BankAccount:
pass
class ShoppingCart:
pass
# Method names: snake_case
def get_balance(self):
pass
def calculate_total(self):
pass
# Private attributes: leading underscore
self._internal_value = 10
self.__very_private = 20
11. Summary
| Concept | Description | Example |
|---|---|---|
| Class | Blueprint for objects | class Dog: |
| Object | Instance of a class | dog = Dog() |
| init | Constructor method | def __init__(self): |
| self | Reference to instance | self.name = name |
| Instance attribute | Unique to each object | self.name |
| Class attribute | Shared by all instances | Dog.species |
| @property | Getter/setter decorator | @property def name(): |
| @classmethod | Method that receives class | @classmethod def create(): |
| @staticmethod | Independent method | @staticmethod def util(): |
- Classes group data and behavior together
- Use
__init__to initialize objects - Instance attributes are unique per object
- Class attributes are shared across instances
- Use properties for controlled attribute access
- Follow naming conventions and design principles
- Keep classes focused (Single Responsibility)
12. What's Next?
In Module 11 - OOP Advanced, you'll learn:
- Inheritance and method overriding
- Multiple inheritance
- Polymorphism
- Abstract base classes
- Method Resolution Order (MRO)
13. Practice Exercises
Exercise 1: Create a Rectangle Class
Implement a Rectangle class with width, height, area, and perimeter calculations.
Exercise 2: Student Management System
Create Student and Course classes with enrollment functionality.
Exercise 3: Library System
Build Book and Library classes with checkout/return functionality.
Exercise 4: Temperature Converter
Create a Temperature class with Celsius, Fahrenheit, and Kelvin conversions.
Exercise 5: Banking System
Expand the BankAccount class with transfer, transaction history, and overdraft protection.
Try solving these exercises on your own first. Solutions will be provided in the practice section.