Object Oriented Programming

1. 🧱 Introduction to OOP

    1.1 What is OOP?
    1. OOP stands for Object-Oriented Programming. Imagine you are creating a FIFA-style football game. If we only use normal functions, the code can become messy because we need to manage many things: - Players - Teams - Matches - Scores - Player positions - Player actions OOP helps us organize this kind of complex program in a cleaner way. In OOP, we create classes and objects. A class is like a blueprint. An object is an actual thing created from that blueprint. Example: Player class ➜ Blueprint for players Messi object ➜ One actual player Ronaldo object ➜ Another actual player For a FIFA-style game, we can create classes like: - Player - Team - Match - Stadium - Scoreboard
    1.2 Class and Object
    1. A class defines what an object should have and what it can do. An object is created from a class. Example: Class: Player Objects: messi, ronaldo, mbappe The Player class can define common details for every player. Each player object can have its own values.
    1.3 Attributes and Methods
    1. Objects usually have attributes and methods. Attributes are data related to an object. Methods are actions that an object can perform. In simple words: - Attribute is like a variable inside an object. - Method is like a function inside a class. Example for Player class:Attributes: - name - age - position - country - speed Methods: - run() - kick() - pass_ball() - tackle() - shoot() Example: Player object: name = "Messi" age = 36 position = "Forward" country = "Argentina" Methods: run() shoot() pass_ball()
    1.4 Why Use OOP?
    1. OOP is useful because it helps us: ✅ Organize complex code ✅ Group related data and behavior together ✅ Reuse code using inheritance ✅ Create real-world models in code ✅ Make code easier to maintain and extend Example: Instead of writing many separate functions for each player, we can create one Player class and create many player objects from it.

2. 🧩 Creating Classes and Objects

    2.1 Creating a Class
    1. We use the class keyword to define a class. Class names are usually written in CamelCase. Syntax: class ClassName:# attributes and methodsExample: class Car:pass Here: Car ➜ class name
    2.2 __init__ Method
    1. The __init__ method is called a constructor. It runs automatically when an object is created. We usually define attributes inside __init__. Example: class Car:def __init__(self, brand, model, color):self.brand = brandself.model = modelself.color = color Here: brand, model, color ➜ attributes self.brand, self.model, self.color ➜ object attributes
    2.3 What is self?
    1. self refers to the current object. It is used to access attributes and methods inside the class. Example: self.brand means brand of the current object 📌 self is automatically passed when we call a method using an object.
    2.4 Creating Object from Class
    1. Example: class Car:def __init__(self, brand, model, color="blue"):self.brand = brandself.model = modelself.color = colordef display_info(self):return f"{self.brand} {self.model} in {self.color}"my_car = Car("Toyota", "Corolla", "Red") print(my_car.display_info())Output: Toyota Corolla in Red Here: Car ➜ class my_car ➜ object brand, model, color ➜ attributes display_info() ➜ method __init__() ➜ constructor that runs automatically
    2.5 Multiple Objects
    1. We can create many objects from the same class. Example: class Car:def __init__(self, brand, model, color="blue"):self.brand = brandself.model = modelself.color = colordef display_info(self):return f"{self.brand} {self.model} in {self.color}"car_1 = Car("Toyota", "Corolla", "Red") car_2 = Car("Honda", "Civic")print(car_1.display_info()) print(car_2.display_info())Output: Toyota Corolla in Red Honda Civic in blue 📌 Same class can create many objects with different values.

3. 🧠 Principles of OOP

    3.1 Four Main Principles
    1. There are four main principles of OOP: 1. Inheritance 2. Polymorphism 3. Encapsulation 4. Abstraction These principles help us write organized, reusable, and maintainable code.
    3.2 Inheritance
    1. Inheritance allows one class to reuse attributes and methods from another class. Example idea: Person ➜ Parent class Employee ➜ Child class Employee can reuse common details from Person, such as name, date of birth, and age.
    3.3 Polymorphism
    1. Polymorphism means same method name or same operator can behave differently for different objects. Example: A Nepali person may greet using Namaste. An English person may greet using Hello. Both can have the same method name: greet() but the output can be different. 📌 In Python, polymorphism is commonly seen through method overriding and operator overloading.
    3.4 Encapsulation
    1. Encapsulation means keeping data and methods together inside a class. It also helps control how data is accessed and modified. Example: A bank account should not allow direct access to balance. Instead, we use methods like: deposit() withdraw() get_balance()
    3.5 Abstraction
    1. Abstraction means hiding complex internal details and showing only what is necessary. Example: When we drive a car, we use steering, brake, and accelerator. We do not need to understand how the engine works internally. In Python, when we use: "HELLO".lower() we know it converts text to lowercase. We do not need to know the internal logic of how lower() works.

4. 🧬 Inheritance

    4.1 What is Inheritance?
    1. Inheritance allows a new class to use attributes and methods from an existing class. The existing class is called parent class or base class. The new class is called child class or derived class. Inheritance helps us: ✅ Reuse code ✅ Avoid duplication ✅ Create relationship between classes ✅ Extend existing functionality
    4.2 Types of Inheritance
    1. Common types of inheritance: Single inheritance: A ➜ B Example: Person ➜ Employee Multilevel inheritance: A ➜ B ➜ C Example: Person ➜ Employee ➜ Manager Multiple inheritance: A + B ➜ C Example: Teacher + Researcher ➜ Professor
    4.3 Inheritance Example
    1. Example: class Person:def __init__(self, name, birth_year):self.name = nameself.birth_year = birth_year@propertydef age(self):return 2026 - self.birth_yeardef is_adult(self):return self.age >= 18class Employee(Person):def __init__(self, name, birth_year, employee_id, department):super().__init__(name, birth_year)self.employee_id = employee_idself.department = departmentemployee_1 = Employee("Rabindra", 1993, "E001", "IT")print(employee_1.name) print(employee_1.age) print(employee_1.employee_id) print(employee_1.is_adult()) Here: Person ➜ parent class Employee ➜ child class name, birth_year, age, is_adult() ➜ inherited from Person employee_id, department ➜ defined in Employee 📌 @property allows us to use a method like an attribute. 📌 Because of @property, we can write employee_1.age instead of employee_1.age()
    4.4 super()
    1. super() is used to call the parent class method. In the example: super().__init__(name, dob) This calls the __init__ method of the Person class. 📌 super() helps us reuse parent class initialization code.

5. 🔁 Polymorphism

    5.1 What is Polymorphism?
    1. Polymorphism means one method name can behave differently depending on the object. Imagine different people greeting in different languages. A Nepali person says: Namaste An English person says: Hello Both are greeting, but the way of greeting is different. In OOP, this can be represented using the same method name with different behavior.
    5.2 Polymorphism Example
    1. Example: class Nepali:def __init__(self, name):self.name = namedef greet(self):return f"Namaste, {self.name}!"class English:def __init__(self, name):self.name = namedef greet(self):return f"Hello, {self.name}!"nepali_person = Nepali("Rabindra") english_person = English("John")print(nepali_person.greet()) print(english_person.greet())Output: Namaste, Rabindra! Hello, John!
    5.3 Polymorphism with Loop
    1. Example: people = [nepali_person, english_person]for person in people:print(person.greet())Output: Namaste, Rabindra! Hello, John! Here: Both objects have greet() method. Python calls the correct greet() method depending on the object. 📌 In Python, objects do not always need to come from the same parent class for polymorphism. 📌 If different objects have the same method name, Python can call that method.

6. 🔄 Method Overriding

    6.1 What is Method Overriding?
    1. Method overriding happens when a child class defines a method with the same name as a method in the parent class. The child class method replaces the parent class method for child objects. Example: Animal has speak() Dog also has speak() When we call speak() on Dog object, Dog's version runs.
    6.2 Method Overriding Example
    1. Example: class Animal:def speak(self):return "Animal speaks"class Dog(Animal):def speak(self):return "Woof!"my_animal = Animal() my_dog = Dog()print(my_animal.speak()) print(my_dog.speak())Output: Animal speaks Woof! Here: Dog has its own speak() method. So Dog overrides the speak() method from Animal.

7. ➕ Operator Overloading

    7.1 What is Operator Overloading?
    1. Operator overloading allows operators to behave differently for custom objects. Example: + adds numbers: print(10 + 20)Output: 30 + concatenates strings: print("Hello" + "World")Output: HelloWorld For our own class, we can define what + should do. This is called operator overloading.
    7.2 Operator Overloading with __add__()
    1. Example: class Vector2D:def __init__(self, x, y):self.x = xself.y = ydef __add__(self, other):return Vector2D(self.x + other.x, self.y + other.y)def __repr__(self):return f"Vector2D({self.x}, {self.y})"v1 = Vector2D(1, 2) v2 = Vector2D(3, 4)v3 = v1 + v2print(v3)Output: Vector2D(4, 6) Here: v1 + v2 internally calls: v1.__add__(v2) 📌 __add__ defines how + works for Vector2D objects.
7.3 Common Operator Methods
OperatorInternal Method
+__add__
-__sub__
*__mul__
/__truediv__
//__floordiv__
%__mod__
**__pow__
==__eq__
!=__ne__
<__lt__
>__gt__
<=__le__
>=__ge__
print()__str__ or __repr__
len()__len__

8. 🔐 Encapsulation

    8.1 What is Encapsulation?
    1. Encapsulation means keeping data and methods together inside a class. It also means controlling how data is accessed or changed. Example: A bank account has balance. We should not directly change balance from outside. Instead, we should use methods like: deposit() withdraw() get_balance() This helps protect data from invalid changes.
    8.2 Public, Protected, and Private Naming
    1. In Python: public_name: Normally accessible from outside the class. _name: Treated as internal or protected by convention. __name: Triggers name mangling. 📌 Python does not have strict private variables like some other languages. 📌 Double underscore makes accidental access harder, but not impossible.
    8.3 Name Mangling
    1. When we use double underscore, Python changes the name internally. Example: __balance inside BankAccount becomes something like: _BankAccount__balance This is called name mangling. Because of this: account.__balance does not work directly.
    8.4 Encapsulation Example
    1. Example: class BankAccount:def __init__(self, owner, balance=0):self.owner = ownerself.__balance = balancedef deposit(self, amount):if amount > 0:self.__balance = self.__balance + amountelse:print("Deposit amount must be positive")def withdraw(self, amount):if 0 < amount <= self.__balance:self.__balance = self.__balance - amountelse:print("Invalid withdrawal amount")def get_balance(self):return self.__balanceaccount = BankAccount("Rabindra", 1000)account.deposit(500) account.withdraw(200)print(account.get_balance())Output: 1300 Trying direct access: print(account.__balance)Output: AttributeError: 'BankAccount' object has no attribute '__balance' 📌 We access balance using get_balance() instead of directly accessing __balance.

9. 🎭 Abstraction

    9.1 What is Abstraction?
    1. Abstraction means hiding complex details and showing only important features. Imagine driving a car. You use: - Steering wheel - Brake - Accelerator - Gear You do not need to understand the full internal engine system to drive the car. Abstraction works in a similar way. It lets users interact with simple methods while hiding complex internal logic.
    9.2 Abstraction in Python
    1. Example: text = "HELLO" print(text.lower())Output: hello We know lower() converts text to lowercase. But we do not need to know how lower() works internally. This is abstraction.
    9.3 Abstraction in OOP
    1. In OOP, abstraction means we expose simple methods for users and hide complex internal details inside the class. Example: BankAccount class may provide: deposit() withdraw() get_balance() The user only needs to call these methods. The internal balance update logic is hidden inside the class.

Practice QuestionsNot started

  1. 1. Creating Classes

    Question 1 of 5

      1.1 Create a class Car with the following details:
      1. Attributes: brand, model, color. Default value of color should be blue.
      2. Methods: display_info, start_engine, stop_engine. display_info should return: Brand: {brand}, Model: {model}, Color: {color} start_engine should return: Engine of {self.brand} {self.model} started stop_engine should return: Engine of {self.brand} {self.model} stopped
      3. Create two objects of Car class: car_1 (brand=Toyota, model=Corolla, color=Red) and car_2 (brand=Honda, model=Civic).
      4. Call display_info, start_engine, and stop_engine for both objects and display the output.
  2. 2. Inheritance

    Question 2 of 5

      2.1 Create a class Employee with attributes name, dob, salary, skill_sets, and property age. The age property should calculate age from dob.
      1. Create class Developer inheriting Employee. Additional attributes: github_link, is_fullstack. Method: show_profile that returns {name} has skills: s1, s2. Git: {github_link}.
      2. Create class HR inheriting Employee. Additional attributes: is_manager, onboard_rate. Method: show_profile that returns {name} has skills: s1, s2. Onboard rate: {onboard_rate}.
      3. 5 developers and 2 HR employees joined the company. Create objects for them with sample attributes.
      4. Create list developers_list with all developer objects. Create list hr_list with all HR objects.
      5. Loop through developers_list and display developers who have both Python and Git skills.
  3. 3. Polymorphism and Method Overriding

    Question 3 of 5

      3.1 Create a class Animal with method speak that returns Animal speaks.
      1. Create class Dog inheriting Animal. Override speak method to return Woof!
      2. Create class Cat inheriting Animal. Override speak method to return Meow!
      3. Create objects of Animal, Dog, and Cat.
      4. Store all objects in a list.
      5. Loop through the list and call speak() for each object.
  4. 4. Point Class and Operator Overloading

    Question 4 of 5

      4.1 Create a class Point with attributes x_coordinate, y_coordinate, and method dist_from_origin using formula sqrt(x^2 + y^2).
      1. Define __str__ so that print(point) displays: P(x, y)
      2. Overload + and - operators. For addition, p_1 + p_2 should return a new Point object with coordinates (x1 + x2, y1 + y2). For subtraction, p_1 - p_2 should return a new Point object with coordinates (x1 - x2, y1 - y2).
      3. Overload comparison operators: <, >, <=, >=, ==, !=. Comparison should be based on dist_from_origin.
      4. Create two points: p_1 = Point(3, 4) and p_2 = Point(1, 2).
      5. Use print(p_1) and print(p_2). Expected output: P(3, 4) and P(1, 2).
      6. Create a new point p_3 = p_1 + p_2. Display p_3.
      7. Calculate distance of p_1 and p_2 from origin using dist_from_origin.
      8. Compare p_1 and p_2 using overloaded comparison operators. Example: p_1 > p_2, p_1 <= p_2
  5. 5. Encapsulation

    Question 5 of 5

      5.1 Create a class BankAccount with attributes owner, __balance (private, default value 0).
      1. Methods: deposit, withdraw, get_balance, transfer. deposit: Add amount to balance if the amount is positive. withdraw: Subtract amount from balance if the amount is positive and less than or equal to balance. get_balance: Return current balance. transfer: Transfer amount from one account to another if the amount is valid and balance is sufficient.
      2. Create two accounts: account_1 (owner=Rabindra) and account_2 (owner=John).
      3. Deposit 1000 to account_1.
      4. Deposit 500 to account_2.
      5. Withdraw 200 from account_1.
      6. Withdraw 100 from account_2.
      7. Transfer 200 from account_1 to account_2.
      8. Display final balance of both accounts.