Object-oriented programming (OOP) is a programming paradigm centred around the concept of objects.
Table of Contents
What is a Class?
- A class is a blueprint or template for creating objects.
- It defines the attributes(data members) and behaviours(methods) that objects will have.
- Classes are a fundamental concept in object-oriented programming (OOP).
- It’s a real-world entity
For example, if you want to model a Car, you would create a Car class that might have properties like colour, make, and model, and methods (functions) like start_engine() or drive().
# Defining a class
class Car:
def __init__(self, make, model, color):
self.make = make # Attribute
self.model = model # Attribute
self.color = color # Attribute
def start_engine(self): # Method
print(f"The {self.color} {self.make} {self.model}'s engine has started.")
def drive(self): # Method
print(f"The {self.color} {self.make} {self.model} is now driving.")
What is an Object?
- Object is an instance of a class.
- All data members and member functions of the class can be accessed with the help of objects.
- When a class is defined, no memory is allocated, but memory is allocated when it is instantiated (i.e. an object is created).
my_car = Car(make="Toyota", model="Camry", color="Red")
# Accessing attributes and methods
print(my_car.make) # Output: Toyota
my_car.start_engine() # Output: The Red Toyota Camry's engine has started.
my_car.drive() # Output: The Red Toyota Camry is now driving.
OOP Concept
Object-oriented programming (OOP) is about classes and objects. OOP enables us to model real-world entities, making code more modular, reusable, and easier to maintain. Four fundamental principles—abstraction, encapsulation, inheritance, and polymorphism—form the backbone of OOP. Understanding these principles is crucial for writing effective and efficient object-oriented code.
Encapsulation
- Wrapping data (attributes) and functions(methods) together in a single unit is called encapsulation.
- It restricts access to certain components of an object to protect the integrity of the data and prevent unauthorized or accidental interference.
- Private Members: In many OOP languages, you can make data members private, so they cannot be accessed from outside the class. For example, in Python, you can prefix an attribute with an underscore (
_
) to indicate it is private by convention only.
class BankAccount:
def __init__(self, balance):
self.__balance = balance
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient funds")
def get_balance(self):
return self.__balance
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
In the above code, we have wrapped all attributes and methods related to the Bank account as a single unit.
Inheritance
- It allows a new class to inherit properties and behaviour from an existing class.
- It helps in reusing code and establishing a natural hierarchy between classes.
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
def start(self):
return f"{self.make} {self.model} is starting."
class Car(Vehicle):
def __init__(self, make, model, doors):
super().__init__(make, model)
self.doors = doors
def open_doors(self):
return f"Opening {self.doors} doors."
my_car = Car("Honda", "Civic", 4)
print(my_car.start()) # Output: Honda Civic is starting.
print(my_car.open_doors()) # Output: Opening 4 doors.
The Car doesn’t have method start(), But the Car object is using start() due to inheritance
Polymorphism
- Polymorphism means “many forms” and allows methods to do different things based on the object it is acting upon, even if they share the same name.
- The ability of a message to be displayed in more than one form.
- It can be achieved through method overloading, method overriding, and operator overloading
Example: A person can have different characteristics at the same time. Like a man at the same time is a father, a husband, and an employee. So the same person possesses different behaviors in different situations. This is called polymorphism.
Method Overloading
- Method overloading allows a class to have multiple methods with the same name but with different signatures(number, type, or order).
- Compile-time Polymorphism
- Python does not natively support method overloading in the same way as some other languages (like Java or C++). Read further
add(int item1, int item2)
add(int item1, int item2,int item3)
add(string item1, string item2)
Method Overriding
- Method overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class.
- Runtime polymorphism
class Animal:
def speak(self):
return "Some generic sound"
class Dog(Animal):
def speak(self):
return "Bark"
class Cat(Animal):
def speak(self):
return "Meow"
def make_animal_speak(animal):
print(animal.speak())
my_dog = Dog()
my_cat = Cat()
make_animal_speak(my_dog) # Output: Bark
make_animal_speak(my_cat) # Output: Meow
In this example, the sound method is defined in both the Animal class and its subclasses Dog and Cat. When the sound method is called on an object of the Dog or Cat class, the overridden method in the respective subclass is executed instead of the one in the Animal class.
Abstraction
- This principle involves hiding complex implementation details and showing only the essential features of an object.
- This allows the user to interact with an object without needing to understand its internal workings.
- In many OOP languages, abstract classes or interfaces are used to implement abstraction.
- An abstract class cannot be instantiated and often includes one or more abstract methods that must be implemented by derived classes.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.1416 * self.radius * self.radius
my_circle = Circle(5)
print(my_circle.area()) # Output: 78.54
Advantages of OOP
- Modularity: Code is organized into distinct classes, making it easier to manage and maintain.
- Reusability: Inheritance and polymorphism promote code reuse.
- Flexibility: Encapsulation and abstraction allow for changes in code with minimal impact on other parts of the program.
- Scalability: OOP provides a structured way to develop complex software that can be easily scaled.
Association, Aggregation, and Composition
Association
- Association represents a general relationship between two or more objects where they interact with each other. It is the most basic form of a relationship.
- Bidirectional or Unidirectional: An association can be one-way (unidirectional) or two-way (bidirectional).
- Multiplicity: Specifies how many instances of one class are associated with one instance of another class (e.g., one-to-one, one-to-many).
- Example: A
Teacher
can be associated with multipleStudents
, and aStudent
can be associated with multipleTeachers
.
Example: A Driver
and a Car
have an association. A Driver
drives a Car
, and a Car
can be driven by a Driver
.
class Driver:
def __init__(self, name):
self.name = name
def drives(self, car):
print(f"{self.name} drives a {car.model}")
class Car:
def __init__(self, model):
self.model = model
# Example usage:
driver = Driver("Alice")
car = Car("Toyota Corolla")
driver.drives(car)
Output: Alice drives a Toyota Corolla
In this case, Driver
and Car
are associated, but they can exist independently.
Aggregation
- Aggregation is a specialized form of association, also known as a “has-a” relationship.
- It represents a whole-part relationship where the part can exist independently of the whole.
- Weak relationship: The lifecycle of the part is independent of the whole. If the whole object is destroyed, the part can still exist.
- Visual Representation: In UML diagrams, aggregation is represented by a hollow diamond.
- Example: A
Library
aggregatesBooks
. Even if theLibrary
is closed or demolished, theBooks
can still exist independently.
Example: A Library aggregates Books. Books belong to the library, but they can exist without the library.
class Book:
def __init__(self, title):
self.title = title
class Library:
def __init__(self, name):
self.name = name
self.books = []
def add_book(self, book):
self.books.append(book)
def show_books(self):
for book in self.books:
print(book.title)
# Example usage:
library = Library("City Library")
book1 = Book("1984 by George Orwell")
book2 = Book("To Kill a Mockingbird by Harper Lee")
library.add_book(book1)
library.add_book(book2)
library.show_books()
Output
1984 by George Orwell
To Kill a Mockingbird by Harper Lee
In this example, the Library contains Books, but the Books can exist even if the Library is destroyed.
Composition
- Composition is a more restrictive form of aggregation.
- It represents a whole-part relationship where the part cannot exist independently of the whole.
- Strong relationship: The lifecycle of the part is strictly tied to the lifecycle of the whole. If the whole object is destroyed, the part is also destroyed.
- Visual Representation: In UML diagrams, composition is represented by a filled diamond.
- Example: A
House
and itsRooms
. If theHouse
is destroyed, itsRooms
do not exist independently.
Example: A House is composed of Rooms. If the house is demolished, the rooms cease to exist.
class Room:
def __init__(self, name):
self.name = name
class House:
def __init__(self, address):
self.address = address
self.rooms = []
def add_room(self, room_name):
room = Room(room_name)
self.rooms.append(room)
def show_rooms(self):
for room in self.rooms:
print(f"Room: {room.name}")
# Example usage:
house = House("123 Maple Street")
house.add_room("Living Room")
house.add_room("Bedroom")
house.show_rooms()
Output:
Room: Living Room
Room: Bedroom
In this example, the House is composed of Rooms. If the House object is destroyed, the Rooms no longer exist.
Association | Aggregation | Composition |
General Relationship | Whole-Part Relationship | Strong Whole-Part Relationship |
No strong dependency | Objects are still relatively independent | Strong dependency between objects |
Objects can exist independently | The lifecycle of the part is not tied to the whole | The lifecycle of the part is strictly tied to the whole. |
Represents a “uses-a” or “knows-a” relationship | Represents a “has-a” relationship with some ownership | Represents a “contains-a” or “is-part-of” relationship |
Hierarchy Analogy:
- An association can be viewed as the most general concept, where objects have some kind of relationship.
- Aggregation is a more specific type of Association where there is a whole-part relationship, but the parts are independent.
- Composition is the most specific and strongest type of relationship, where the parts are entirely dependent on the whole.
Association
|
|-- Aggregation
| |
| |-- Composition
- Association is the broadest concept, encompassing any kind of relationship between objects.
- Aggregation is a specific type of Association where one object is a part of another but with less dependency.
- Composition is a specific type of Aggregation with the highest level of dependency between the whole and its parts.
Inheritance vs Association
Inheritance
- Inheritance is a mechanism where a new class (derived or child class) inherits the properties and behaviours (attributes and methods) of an existing class (base or parent class).
- It represents an “is-a” relationship.
- Purpose: Inheritance promotes code reusability by allowing a child class to reuse methods and attributes of the parent class. The child class can also override or extend the functionality of the parent class.
- Example:
- The dog is an Animal
- A student is a Person
Association
- The association represents a relationship where two or more classes are connected but remain independent of each other.
- It represents a “has-a” relationship and is used to show how objects interact with each other.
- Purpose: Association describes how objects work together, but each object has its own lifecycle and can exist independently of the other.
- Example
- The book has a Page
- The car has an engine
Inheritance
class Animal:
def speak(self):
..........
class Dog(Animal):
def speak(self):
..........
Association
class Teacher:
def some_function(self, student):
..........
class Student:
def learn(self, teacher):
Teacher().some_function()
Important Concepts
A superclass reference variable can indeed refer to a subclass object
Java
Java/C++: A superclass reference variable can hold a reference to any subclass object, and the method that gets called depends on the actual object type at runtime.
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // Superclass reference to a subclass object
myAnimal.sound(); // Output will be "Dog barks"
}
}
Here, myAnimal is a reference variable of the Animal superclass, but it refers to an instance of the Dog subclass. When you call the sound() method on myAnimal, the Dog class’s version of sound() is executed, demonstrating polymorphism.
Python
Since variables are dynamically typed, any variable can hold any object. The concept of a superclass reference holding a subclass object is not explicitly enforced, but the effect is similar due to Python’s dynamic typing and polymorphism.
class Animal:
def sound(self):
print("Animal makes a sound")
class Dog(Animal):
def sound(self):
print("Dog barks")
class Cat(Animal):
def sound(self):
print("Cat meows")
my_animal = Dog() # my_animal now references a Dog object
my_animal.sound() # Output: Dog barks
my_animal = Cat() # my_animal now references a Cat object
my_animal.sound() # Output: Cat meows
In Python, my_animal can refer to any object, including an object of a subclass, and Python will determine at runtime which sound() method to call:
Conclusion
Object-Oriented Programming (OOP) is a powerful and flexible paradigm that allows developers to create modular, reusable, and scalable software. By organizing code around objects—instances of classes—OOP makes it easier to model real-world problems and manage complex systems. The four fundamental principles of OOP—encapsulation, inheritance, polymorphism, and abstraction—provide a robust framework for designing and implementing software.
Polymorphism, in particular, plays a crucial role in OOP by allowing objects to be treated as instances of their parent class, enabling method overloading and overriding. These techniques enhance the flexibility and maintainability of code, allowing for the seamless extension and modification of software without disrupting existing functionality.
By mastering OOP concepts, developers can write more efficient, organized, and maintainable code, making it easier to collaborate on large projects and adapt to future requirements. Whether you are building small applications or large-scale systems, OOP principles serve as a foundational approach that can be applied across various programming languages and development environments.