Object-Oriented Programming (OOP) - Part 1
OOP is like building with LEGO blocks. Each type of LEGO piece (class) can be used to build many specific objects (instances). A 'Car' blueprint (class) can create many actual cars (objects), each with the same basic structure but different colors, models, and features.
**Object‑Oriented Programming (OOP)** is a programming paradigm that organises code around **objects** (which hold data) and **classes** (which are blueprints for objects). Instead of writing linear, step‑by‑step code, you model real‑world entities as objects with attributes (data) and methods (behaviours).
## Core Concepts (Four Pillars of OOP)
1. **Encapsulation** – Bundling data and methods that operate on that data within a single unit (a class). It also restricts direct access to some of an object's components (using private attributes like `_variable`).
2. **Inheritance** – Creating a new class based on an existing class, reusing and extending its functionality (covered in Part 2).
3. **Polymorphism** – The ability of different classes to respond to the same method call in their own way (covered in Part 2).
4. **Abstraction** – Hiding complex implementation details and showing only essential features.
## Classes and Objects
A **class** is a blueprint – it describes what attributes and methods an object will have. An **object** is an instance of a class, created from that blueprint.
```python
class Dog: # class definition
def __init__(self, name): # constructor
self.name = name # instance attribute
def bark(self): # instance method
return f"{self.name} says Woof!"
d = Dog("Buddy") # object instantiation
print(d.bark()) # method call
```
## The `__init__` Constructor
The `__init__` method is called automatically when you create an object. It initialises the object's attributes. The `self` parameter refers to the current instance – it must be the first parameter of any instance method. You can give it any name, but `self` is the convention.
## Instance Attributes vs. Class Attributes
- **Instance attributes** – unique to each object, defined inside `__init__` using `self.attribute`.
- **Class attributes** – shared across all instances, defined directly inside the class body.
```python
class Student:
school = "Python High" # class attribute
def __init__(self, name):
self.name = name # instance attribute
```
## Instance Methods, Class Methods, and Static Methods
- **Instance methods** – take `self` and can access/modify instance attributes.
- **Class methods** – take `cls` and can modify class attributes; use `@classmethod` decorator.
- **Static methods** – take neither `self` nor `cls`; use `@staticmethod`; they behave like regular functions but belong to the class namespace.
## The `@property` Decorator (Getters and Setters)
The `@property` decorator allows you to define methods that can be accessed like attributes. This lets you add validation or computation behind a simple attribute syntax.
```python
class Temperature:
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Too cold!")
self._celsius = value
```
## Encapsulation and Name Mangling
By convention, attributes starting with an underscore `_` are "protected" (internal use). Double underscore `__` triggers name mangling to avoid accidental overrides in subclasses.
## Why Use OOP?
- **Modularity** – classes are self‑contained, making code easier to understand and maintain.
- **Reusability** – you can reuse classes across projects.
- **Data hiding** – encapsulation protects internal state.
- **Modelling real‑world problems** – natural mapping from problem domain to code.
## Common Mistakes
- Forgetting `self` as the first parameter of instance methods.
- Using `self` incorrectly in class/static methods.
- Confusing instance and class attributes (changing a class attribute via an instance creates a new instance attribute instead of modifying the class attribute).
- Not calling `__init__` of parent class when using inheritance (covered in Part 2).
## Practice Exercises
1. Create a `Car` class with attributes `brand`, `model`, `year`, and a method `start_engine()` that prints a message.
2. Add a class attribute `number_of_wheels = 4` and create a method that returns it.
3. Write a `BankAccount` class (similar to example 2) with deposit, withdraw, and a `@property` for balance that prevents negative values.
4. Use `@property` to create a `Circle` class where `diameter` is calculated from `radius`.
5. Create a simple `Library` system (like example 5) with books, members, and borrowing limits.
## Real‑World Applications
- Game development (characters, enemies, items).
- Web frameworks (Django, Flask use classes for models, views, forms).
- GUI applications (buttons, windows, text fields are objects).
- Data processing pipelines (each step is an object).
- Simulations (agents, environments).
This lesson covers **5 complete examples** to solidify these concepts. Part 2 will cover inheritance, polymorphism, and more advanced OOP topics.
# Object-Oriented Programming - Part 1
print("OBJECT-ORIENTED PROGRAMMING (OOP) - PART 1")
print("=" * 60)
# Example 1: Creating a simple class
print("\n1. CREATING A SIMPLE CLASS")
print("-" * 30)
class Dog:
"""A simple Dog class"""
# Constructor method (initializer)
def __init__(self, name, age, breed):
"""Initialize dog attributes"""
self.name = name # Instance variable
self.age = age # Instance variable
self.breed = breed # Instance variable
# Instance method
def bark(self):
"""Make the dog bark"""
return f"{self.name} says: Woof!"
def get_info(self):
"""Get dog information"""
return f"{self.name} is a {self.age}-year-old {self.breed}"
def have_birthday(self):
"""Celebrate dog's birthday"""
self.age += 1
return f"Happy Birthday {self.name}! Now {self.age} years old."
# Creating objects (instances) from the class
print("Creating dog objects:")
dog1 = Dog("Buddy", 3, "Golden Retriever")
dog2 = Dog("Luna", 2, "German Shepherd")
print(f"Dog 1: {dog1.get_info()}")
print(f"Dog 2: {dog2.get_info()}")
print(f"\n{dog1.bark()}")
print(f"{dog2.bark()}")
# Calling methods
print(f"\n{dog1.have_birthday()}")
print(f"Updated: {dog1.get_info()}")
# Accessing attributes directly
print(f"\nDirect attribute access:")
print(f"Dog1's name: {dog1.name}")
print(f"Dog2's breed: {dog2.breed}")
# Example 2: Bank Account class
print("\n\n2. BANK ACCOUNT CLASS")
print("-" * 30)
class BankAccount:
"""A simple bank account class"""
# Class variable (shared by all instances)
bank_name = "Python Bank"
def __init__(self, account_holder, initial_balance=0):
self.account_holder = account_holder
self.balance = initial_balance
self.account_number = self._generate_account_number()
def _generate_account_number(self):
"""Private method to generate account number"""
import random
return f"ACC{random.randint(10000, 99999)}"
def deposit(self, amount):
"""Deposit money into account"""
if amount > 0:
self.balance += amount
return f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}"
return "Deposit amount must be positive"
def withdraw(self, amount):
"""Withdraw money from account"""
if amount > self.balance:
return "Insufficient funds!"
elif amount <= 0:
return "Withdrawal amount must be positive"
else:
self.balance -= amount
return f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}"
def check_balance(self):
"""Check current balance"""
return f"Account balance: ${self.balance:.2f}"
def transfer(self, amount, other_account):
"""Transfer money to another account"""
if amount > self.balance:
return "Transfer failed: Insufficient funds"
# Withdraw from this account
self.balance -= amount
# Deposit to other account
other_account.balance += amount
return f"Transferred ${amount:.2f} to {other_account.account_holder}"
def get_statement(self):
"""Get account statement"""
return f"""
{self.bank_name}
Account Holder: {self.account_holder}
Account Number: {self.account_number}
Balance: ${self.balance:.2f}
"""
# Using the BankAccount class
print(f"Bank: {BankAccount.bank_name}")
# Create accounts
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(f"\nCreated accounts:")
print(f"Alice's account: {account1.account_number}")
print(f"Bob's account: {account2.account_number}")
# Perform transactions
print("\nTransactions:")
print(account1.deposit(200))
print(account1.withdraw(150))
print(account1.withdraw(2000)) # Should fail
# Transfer money
print(f"\nBefore transfer:")
print(f"Alice: {account1.check_balance()}")
print(f"Bob: {account2.check_balance()}")
print(f"\n{account1.transfer(300, account2)}")
print(f"\nAfter transfer:")
print(f"Alice: {account1.check_balance()}")
print(f"Bob: {account2.check_balance()}")
# Get statement
print("\nAccount statement:")
print(account1.get_statement())
# Example 3: Class vs Instance variables
print("\n\n3. CLASS VS INSTANCE VARIABLES")
print("-" * 30)
class Student:
# Class variable (shared by all students)
school_name = "Python High School"
total_students = 0
def __init__(self, name, grade):
# Instance variables (unique to each student)
self.name = name
self.grade = grade
Student.total_students += 1 # Modify class variable
self.student_id = f"S{Student.total_students:03d}"
@classmethod
def get_school_info(cls):
"""Class method - works on class, not instance"""
return f"School: {cls.school_name}, Total Students: {cls.total_students}"
@staticmethod
def calculate_average(grades):
"""Static method - doesn't need class or instance"""
if not grades:
return 0
return sum(grades) / len(grades)
def get_info(self):
return f"{self.name} (ID: {self.student_id}), Grade: {self.grade}"
# Create students
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Charlie", 12)
print("Student Information:")
print(f"School: {Student.school_name}")
print(f"Total Students: {Student.total_students}")
print(f"\nIndividual students:")
print(student1.get_info())
print(student2.get_info())
print(student3.get_info())
# Using class method
print(f"\n{Student.get_school_info()}")
# Using static method
grades = [85, 90, 78, 92, 88]
print(f"\nAverage of grades {grades}: {Student.calculate_average(grades):.1f}")
# Example 4: Property decorators (getters/setters)
print("\n\n4. PROPERTY DECORATORS")
print("-" * 30)
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # Private variable convention
@property
def celsius(self):
"""Getter for celsius"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter for celsius with validation"""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Property that calculates Fahrenheit"""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Setter for Fahrenheit"""
self._celsius = (value - 32) * 5/9
# Using properties
temp = Temperature(25)
print(f"Initial: {temp.celsius}°C = {temp.fahrenheit:.1f}°F")
# Change celsius
print("\nChanging celsius to 30:")
temp.celsius = 30
print(f"Now: {temp.celsius}°C = {temp.fahrenheit:.1f}°F")
# Change fahrenheit
print("\nSetting fahrenheit to 100:")
temp.fahrenheit = 100
print(f"Now: {temp.celsius:.1f}°C = {temp.fahrenheit}°F")
# Try invalid temperature
try:
print("\nTrying to set invalid temperature:")
temp.celsius = -300 # Should raise error
except ValueError as e:
print(f"Error: {e}")
# Example 5: Simple OOP project - Library System
print("\n\n5. LIBRARY SYSTEM PROJECT")
print("-" * 30)
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.is_checked_out = False
self.checked_out_by = None
def check_out(self, member_name):
if not self.is_checked_out:
self.is_checked_out = True
self.checked_out_by = member_name
return f"'{self.title}' checked out by {member_name}"
return f"'{self.title}' is already checked out by {self.checked_out_by}"
def return_book(self):
if self.is_checked_out:
member = self.checked_out_by
self.is_checked_out = False
self.checked_out_by = None
return f"'{self.title}' returned by {member}"
return f"'{self.title}' was not checked out"
def __str__(self):
status = "Checked out" if self.is_checked_out else "Available"
if self.is_checked_out:
status += f" by {self.checked_out_by}"
return f"'{self.title}' by {self.author} - {status}"
class Library:
def __init__(self, name):
self.name = name
self.books = []
def add_book(self, book):
self.books.append(book)
print(f"Added: {book.title}")
def find_book(self, title):
for book in self.books:
if book.title.lower() == title.lower():
return book
return None
def list_books(self):
if not self.books:
print("No books in library")
else:
print(f"\nBooks in {self.name}:")
for book in self.books:
print(f" - {book}")
# Create library and books
library = Library("City Library")
book1 = Book("Python Programming", "John Doe", "123456")
book2 = Book("Data Science Basics", "Jane Smith", "789012")
book3 = Book("Web Development", "Bob Johnson", "345678")
library.add_book(book1)
library.add_book(book2)
library.add_book(book3)
# List all books
library.list_books()
# Check out and return books
print("\nChecking out books:")
print(book1.check_out("Alice"))
print(book2.check_out("Bob"))
print(book1.check_out("Charlie")) # Should fail
library.list_books()
print("\nReturning books:")
print(book1.return_book())
library.list_books()
→ Run this code interactively