Skip to main content

Object Oriented Programming

  1. Lesson 1: What Is Object-Oriented Programming (OOP)?
  2. Lesson 2: Classes in Python
  3. Lesson 3: Class and Instance Attributes
  4. Lesson 4: Adding Methods to a Python Class
  5. Class Exercises
  6. Lesson 5: Understanding the self Keyword in Python Classes
  7. self Exercise: Debugging self Usage

Object-Oriented Programming (OOP) in Python

This notebook introduces key concepts of Object-Oriented Programming (OOP) in Python, including classes, attributes, methods, and inheritance. Each section includes examples and exercises.

Lesson 1: What Is Object-Oriented Programming (OOP)?

OOP is a programming paradigm that uses "objects" to design software. Objects combine data (attributes) and functionality (methods) into a single entity.

Key Concepts

A class is a blueprint for creating objects. It defines the structure and behavior that the objects created from the class will have.

Real-Life Example: Consider a "Car" class. It serves as a blueprint for creating car objects. All cars share some common attributes (e.g., make, model, color) and methods (e.g., start, stop).

An object is an instance of a class. It is a specific entity created from a class blueprint.

Real-Life Example: If "Car" is the class, "my_car = Car('Toyota', 'Corolla', 'Blue')" is an object. It represents a specific car with a make "Toyota," model "Corolla," and color "Blue."

Attributes are variables associated with an object. They represent the state or properties of the object.

Real-Life Example: For the "my_car" object, attributes could include "make = 'Toyota'," "model = 'Corolla'," and "color = 'Blue.'"

Methods are functions associated with an object. They define the behavior of the object and operate on its attributes.

Real-Life Example: A car object may have methods such as "start()," "accelerate()," and "stop()." These methods perform actions associated with the car.

Lesson 2: Classes in Python

A class is a blueprint for creating objects. Objects represent specific instances of the class and encapsulate data (attributes) and behavior (methods). Classes allow us to organize and reuse code effectively, making our programs more modular and easier to maintain.

To define a class in Python, we use the class keyword, followed by the class name in PascalCase, and a colon. Inside the class, we define attributes and methods. Attributes store the data for the object, and methods define its behavior.

Real-Life Example: Vehicles

Let's consider a real-world example of a class Vehicle that serves as a blueprint for different types of vehicles like cars, bikes, and trucks. Each vehicle has attributes like make, model, and year, and methods like start_engine or stop_engine.

class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year

def start_engine(self):
print(f"The engine of the {self.year} {self.make} {self.model} is now running.")

def stop_engine(self):
print(f"The engine of the {self.year} {self.make} {self.model} has been turned off.")

# Creating objects of the Vehicle class
car = Vehicle("Toyota", "Camry", 2022)
bike = Vehicle("Harley-Davidson", "Street 750", 2021)

# Using methods of the Vehicle class
car.start_engine()
bike.start_engine()
car.stop_engine()

Real-Life Example: Animals

We can create a class Animal to represent different species. Each animal can have a name, species, and a sound it makes. We can also define a method speak that prints the sound.

class Animal:
def __init__(self, name, species, sound):
self.name = name
self.species = species
self.sound = sound

def speak(self):
print(f"{self.name} the {self.species} says: {self.sound}")

# Creating objects of the Animal class
cat = Animal("Whiskers", "Cat", "Meow Meow")
dog = Animal("Buddy", "Dog", "Woof Woof")
chicken = Animal("Clucky", "Chicken", "Cluck Cluck")

# Using the speak method
cat.speak()
dog.speak()
chicken.speak()

Benefits of Using Classes

  • Code Reusability: Once a class is defined, it can be reused multiple times to create objects with similar characteristics.

  • Encapsulation: Classes bundle data and functionality together, protecting the internal state from being accessed or modified directly.

  • Modularity: Classes help organize code into logical sections, making it easier to read and maintain.

  • Inheritance and Polymorphism: Classes allow code extension through inheritance and enable dynamic method behavior through polymorphism.

Summary

Classes are fundamental to object-oriented programming in Python. They help us model real-world entities and their behavior, enabling us to write structured, reusable, and maintainable code. By practicing with examples like vehicles and animals, you can gain a deeper understanding of how to use classes effectively.

#Lesson 3: Class and Instance Attributes

In Python, attributes are variables that belong to a class or an instance of a class. These attributes can either be instance attributes or class attributes. Understanding the difference between them is essential for writing effective object-oriented programs.

Instance Attributes

Instance attributes are specific to each object (instance) of a class. They are defined inside the init method (or dynamically within other instance methods). Every instance of a class has its own separate copy of these attributes, meaning changes to one instance's attributes do not affect other instances.

Example: Instance Attributes with Animals Let's distinguish between non-instance variables and instance attributes with an example.

class Animal:
# Non-instance attribute (not tied to any object)
non_instance_var = "This belongs to the class but not to any instance"

def __init__(self, name, species, sound):
# Instance attributes
self.name = name
self.species = species
self.sound = sound

def speak(self):
print(f"{self.name} the {self.species} says: {self.sound}")


# Create two instances of Animal
cat = Animal("Whiskers", "Cat", "Meow Meow")
dog = Animal("Buddy", "Dog", "Woof Woof")

# Accessing instance attributes
print(cat.name) # Output: Whiskers
print(dog.name) # Output: Buddy

# Changing instance attributes
cat.name = "Luna"
print(cat.name) # Output: Luna
print(dog.name) # Output: Buddy (remains unchanged)

# Accessing non-instance variable
print(Animal.non_instance_var) # Output: This belongs to the class but not to any instance

Here, name, species, and sound are instance attributes, and they are unique to each object. Modifying them in one instance does not affect other instances.

Class Attributes

Class attributes, on the other hand, are shared by all instances of a class. They are defined directly in the class body and are not tied to any one instance. Class attributes are useful when you want to share data or behavior among all instances of a class.

Example: Class Attributes with Vehicles Let’s explore class attributes using a Vehicle example.

class Vehicle:
# Class attribute shared by all instances
category = "Transport"

def __init__(self, make, model):
# Instance attributes
self.make = make
self.model = model


# Create two instances of Vehicle
car = Vehicle("Toyota", "Camry")
bike = Vehicle("Harley-Davidson", "Street 750")

# Accessing class attribute
print(car.category) # Output: Transport
print(bike.category) # Output: Transport

# Changing the class attribute
Vehicle.category = "Motorized Transport"

# All instances see the updated value
print(car.category) # Output: Motorized Transport
print(bike.category) # Output: Motorized Transport

Overwriting Class Attributes with Instance Attributes

If an instance attribute is defined with the same name as a class attribute, the instance attribute takes precedence. Let’s demonstrate this:

class Vehicle:
category = "Transport" # Class attribute

def __init__(self, make, model):
self.make = make
self.model = model


car = Vehicle("Toyota", "Camry")
bike = Vehicle("Harley-Davidson", "Street 750")

# Overwriting the class attribute for an instance
car.category = "Personal Transport"

print(car.category) # Output: Personal Transport (instance attribute)
print(bike.category) # Output: Transport (class attribute remains unchanged)
print(Vehicle.category) # Output: Transport

Here, changing car.category doesn’t affect the class attribute or the bike instance because the instance car now has its own version of the category attribute.

Example Where Modifying Class Attributes Affects All Instances

When class attributes are modified directly (via the class name), all instances will reflect this change because they share the same attribute in memory.

class Animal:
# Class attribute
kingdom = "Animalia"

def __init__(self, name, species):
self.name = name
self.species = species


# Create two instances
cat = Animal("Whiskers", "Cat")
dog = Animal("Buddy", "Dog")

# Access class attribute
print(cat.kingdom) # Output: Animalia
print(dog.kingdom) # Output: Animalia

# Modify the class attribute
Animal.kingdom = "Fauna"

# All instances reflect the change
print(cat.kingdom) # Output: Fauna
print(dog.kingdom) # Output: Fauna

Need for Class Attributes

Class attributes are useful in the following scenarios:

  1. Shared Data Across Instances: When you want all objects to share a common piece of data. For example, all Animal instances belong to the same kingdom.

  2. Memory Efficiency: Instead of storing the same data for every instance, it is stored once in the class, and all instances reference it.

  3. Behavior Consistency: When you want to enforce the same behavior or characteristic across all instances.

  4. Static Defaults: When you need a default value for an attribute but don’t want to duplicate it for each object.

Summary

  1. Instance Attributes:
  • Unique to each object.
  • Defined in the init method or dynamically.
  • Changes affect only that specific object.
  1. Class Attributes:
  • Shared by all instances of a class.
  • Defined directly in the class body.
  • Changes affect all instances, unless an instance attribute with the same name overrides it.
  • Understanding when to use instance attributes versus class attributes is key to writing clear, efficient, and organized code!

Understanding when to use instance attributes versus class attributes is key to writing clear, efficient, and organized code!

Lesson 4: Adding Methods to a Python Class

Methods are functions that belong to a class and are used to define the behavior of the objects created from that class. Methods operate on the class's attributes and allow objects to perform specific actions.

In this lesson, we’ll focus on instance methods, which are the most common type of methods in Python classes. Instance methods require an object to be called and can access and modify the instance’s attributes.

What Are Instance Methods?

Instance methods are defined inside a class and are called on an instance of that class. They must take self as the first parameter, which refers to the specific instance of the class.

Instance methods can:

  • Access and modify instance attributes.
  • Call other methods of the class.

Defining Instance Methods

Here’s the basic syntax for defining an instance method:

class ClassName:
def instance_method(self, params):
# Method body
pass
  • self: Refers to the specific object that calls the method.
  • params: Optional additional parameters required by the method.

** Example: Adding Methods to the Animal Class

Let’s extend the Animal class by adding some useful instance methods.

class Animal:
def __init__(self, name, species, sound):
self.name = name
self.species = species
self.sound = sound

# Instance method to make the animal speak
def speak(self):
print(f"{self.name} the {self.species} says: {self.sound}")

# Instance method to update the sound of the animal
def change_sound(self, new_sound):
self.sound = new_sound
print(f"{self.name}'s sound has been updated to: {self.sound}")

# Instance method to provide details about the animal
def describe(self):
return f"{self.name} is a {self.species} and says '{self.sound}'."


# Creating objects of the Animal class
cat = Animal("Whiskers", "Cat", "Meow Meow")
dog = Animal("Buddy", "Dog", "Woof Woof")

# Calling instance methods
cat.speak() # Output: Whiskers the Cat says: Meow Meow
dog.speak() # Output: Buddy the Dog says: Woof Woof

# Updating an attribute using an instance method
dog.change_sound("Bark Bark") # Output: Buddy's sound has been updated to: Bark Bark
dog.speak() # Output: Buddy the Dog says: Bark Bark

# Getting animal details
print(cat.describe()) # Output: Whiskers is a Cat and says 'Meow Meow'.
print(dog.describe()) # Output: Buddy is a Dog and says 'Bark Bark'.

Example: Adding Methods to the Vehicle Class Let’s expand the Vehicle class by adding methods to start, stop, and describe the vehicle.

class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.engine_running = False # Attribute to track engine status

# Instance method to start the engine
def start_engine(self):
if not self.engine_running:
self.engine_running = True
print(f"The engine of the {self.year} {self.make} {self.model} is now running.")
else:
print(f"The engine of the {self.year} {self.make} {self.model} is already running.")

# Instance method to stop the engine
def stop_engine(self):
if self.engine_running:
self.engine_running = False
print(f"The engine of the {self.year} {self.make} {self.model} has been turned off.")
else:
print(f"The engine of the {self.year} {self.make} {self.model} is already off.")

# Instance method to describe the vehicle
def describe(self):
return f"This is a {self.year} {self.make} {self.model}."


# Creating objects of the Vehicle class
car = Vehicle("Toyota", "Camry", 2022)
bike = Vehicle("Harley-Davidson", "Street 750", 2021)

# Calling instance methods
car.start_engine() # Output: The engine of the 2022 Toyota Camry is now running.
car.start_engine() # Output: The engine of the 2022 Toyota Camry is already running.
car.stop_engine() # Output: The engine of the 2022 Toyota Camry has been turned off.

# Using the describe method
print(car.describe()) # Output: This is a 2022 Toyota Camry.
print(bike.describe()) # Output: This is a 2021 Harley-Davidson Street 750.

Why Use Instance Methods?

  1. Encapsulation: Methods allow you to group related functionality inside the class, keeping your code organized.
  2. Reusability: Methods can be reused across all instances of the class.
  3. Simplifies Code: Instead of writing repetitive code, you can use methods to define actions that operate on the instance attributes.
  4. Behavior Customization: Each object can perform actions tailored to its own attributes.

Best Practices for Writing Instance Methods

  1. Use self Consistently: Always use self to refer to the instance’s attributes and methods.
  2. Keep Methods Simple: Each method should perform a specific task to ensure readability and maintainability.
  3. Use Meaningful Names: Method names should clearly describe their purpose.
  4. Avoid Redundancy: Don’t repeat logic that can be encapsulated in a method.

Summary

  • Instance methods are functions that belong to a class and require an instance of that class to be called.
  • They operate on instance attributes and can access or modify the internal state of the object.
  • By grouping functionality into instance methods, you can create modular, reusable, and maintainable code.
  • By practicing with examples like Animal and Vehicle, you can understand how to effectively use instance methods to model real-world behaviors in Python!

Class Exercises

Try the following exercises to test your understanding:

  1. Create a Person class with attributes name and age. Add a method introduce() that prints a message introducing the person.
#Exercise 1: Person Class
  1. Library Management System

Create a class Book with the following attributes:

  • title
  • author
  • isbn
  • available_copies

Add methods to:

  • Display book details.
  • Lend a book (decrement available_copies).
  • Return a book (increment available_copies).
#Exercise 2
book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", 5)
book1.display_details() # Displays all book details
book1.lend_book() # Decrements available copies
book1.return_book() # Increments available copies

  1. Bank Account

Create a class BankAccount with attributes

  • account_holder
  • balance
  • account_number

Add methods to:

  • Deposit money (increase balance).
  • Withdraw money (decrease balance, but only if sufficient funds are available).
  • Display account details.
#Exercise 3
account1 = BankAccount("Alice", 1000, "123456789")
account1.deposit(500) # Balance increases to 1500
account1.withdraw(200) # Balance decreases to 1300
account1.display_details() # Displays account holder, balance, and account number

  1. To-Do List

Create a class Task with attributes

  • task_name
  • status (default is incomplete).

Add a method to mark a task as complete.

  • complete_task

Create a class ToDoList to manage multiple tasks. Add methods to:

  • Add a task.
  • Display all tasks with their status.
  • Display only incomplete tasks.
#Exercise 4
task1 = Task("Learn Python")
todo_list = ToDoList()
todo_list.add_task(task1)
todo_list.add_task(Task("Read a book"))

task1.complete_task()

todo_list.display_tasks() # Shows all tasks with their status
todo_list.display_incomplete() # Shows only incomplete tasks

Code Snippets with Bugs

Please fix the following code snippets

#Exercise 1

class Person:
def __init__(self, name):
self.name = name

def greet(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")

# Fix the bug and add an age attribute
p1 = Person("Alice")
p1.greet()

#Exercise 2

class Calculator:
def add(self, a, b):
return a + b

def subtract(self, a, b):
return a - b

# Fix the code to correctly call methods
calc = Calculator()
result = calc.add(5) # Fix this
print(result)

#Exercise 3
class Counter:
def __init__(self):
self.count = 0

def increment(self):
count += 1 # Fix this logic
return self.count

c = Counter()
print(c.increment()) # Should print 1
print(c.increment()) # Should print 2

Lesson 5: Understanding the self Keyword in Python Classes

The self keyword is one of the most important concepts in Python’s object-oriented programming (OOP). However, it can often confuse beginners because of its unique role in classes and methods. Let’s break it down and explain it step by step.

What is self?

  • self is a convention used in Python to represent the current instance of the class.
  • It is the first parameter of instance methods in a class and allows the method to access the instance’s attributes and other methods.
  • When you create an object of a class, Python automatically passes the object itself (self) to the instance methods.
  • Without self, the method wouldn’t know which object’s attributes or methods it should work with.

Why is self Needed?

  1. To Access Instance Attributes
  • The self keyword allows methods to access and modify attributes specific to an instance of the class.
  1. To Distinguish Between Class Attributes and Instance Attributes
  • Without self, Python cannot distinguish whether a variable belongs to the class or is local to the method.
  1. To Maintain Consistency Across All Instance Methods
  • Every instance method needs a reference to the instance it belongs to. Using self ensures this consistency.

How Does self Work?

When you create an object and call a method, Python automatically passes the object itself as the first argument to the method. This is why you must include self as the first parameter in all instance methods.

Example:

class Animal:
def __init__(self, name, species):
self.name = name # Using self to refer to the instance's name
self.species = species # Using self to refer to the instance's species

def speak(self):
print(f"{self.name} the {self.species} says hello!")

# Creating an instance of Animal
dog = Animal("Buddy", "Dog")

# When we call the method speak(), Python automatically passes the instance 'dog' as self
dog.speak() # Output: Buddy the Dog says hello!

In the example above:

  • The self.name refers to the name attribute of the specific instance (e.g., dog).
  • When calling dog.speak(), Python interprets it as Animal.speak(dog). Here, dog becomes the self parameter.

What Happens Without self?

If you forget to include self as the first parameter, Python will throw an error because it doesn’t know how to associate the method with the specific instance.

Example:

class Animal:
def speak(): # Missing self
print("Hello!")

dog = Animal()
dog.speak() # Error: TypeError: speak() takes 0 positional arguments but 1 was given

Here, the error occurs because Python tries to pass the instance dog as the first argument to the method, but the method doesn’t accept any arguments.

Real-Life Analogy

Think of a class as a blueprint for houses. Each house (object) has its own doors, windows, and furniture (attributes). Now imagine a house has a method open_door. The house needs to know which door to open—its own or a neighbor's. The self keyword ensures the method always operates on the current house.

Example: Using self to Access Instance Attributes

Here’s how self helps manage attributes unique to each object:

class Person:
def __init__(self, name, age):
self.name = name # 'self.name' refers to the instance's name
self.age = age # 'self.age' refers to the instance's age

def greet(self):
print(f"Hi, my name is {self.name} and I am {self.age} years old.")

# Creating two instances of Person
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

# Each instance has its own attributes
person1.greet() # Output: Hi, my name is Alice and I am 25 years old.
person2.greet() # Output: Hi, my name is Bob and I am 30 years old.

Here, self.name and self.age refer to the specific object (person1 or person2) that calls the method.

Key Points to Remember About self

  1. Always Include self in Instance Methods:
  • Python automatically passes the instance to the method when it’s called.
  1. self is Not a Keyword:
  • You could technically use any name (e.g., this or obj), but the convention is to use self for clarity and consistency.
  1. self is Required to Access Attributes and Methods:
  • Without self, attributes and methods cannot refer to the specific instance.
  1. Ensures Instance-Specific Behavior:
  • Each object can have unique attributes, and self ensures methods operate on the correct object.

self Exercise: Debugging self Usage

#Exercise 1

class Dog:
def bark():
print("Woof Woof!")

dog1 = Dog()
dog1.bark() # Fix this code

# Exercise 2
class Person:
def __init__(self, name):
name = name # Missing self

def greet(self):
print(f"Hello, my name is {name}") # Fix this code

p = Person("Alice")
p.greet()

#Exercise 3:
class Vehicle:
def start_engine(self):
print("Engine started!")

def start(self):
start_engine() # Fix this code

car = Vehicle()
car.start()

###Summary

  • self refers to the current instance of the class.
  • It allows instance methods to access and modify attributes and call other methods.
  • Python automatically passes the instance as the first argument to instance methods.
  • Without self, methods cannot differentiate between attributes of different objects.
  • Mastering self is the key to understanding and effectively using classes in Python!