Object Oriented Programming

Introduction to OOP

  • Imagine, if you need to write code for FIFA game using function-based approach, it's a big mess.
  • OOP allows us to model complex systems in a more organized and modular way using classes and objects.
  • Objects represent real-world entities that have attributes and methods.
  • Classes are blueprints for creating objects, defining their attributes and behaviors.
  • For FIFA game, we can have classes like Player, Team, Match, etc. Each class can have its own attributes and methods.
  • Analogy: Attribute => Variable, Method => Function
  • For class Player, attributes can be name, age, position, country and methods can be run(), kick(), pass_ball(), tackle(), shoot(), etc.

Principles of OOP

  • There are four main principles that govern object-oriented programming.
  • Inheritance
    1. Allows a class to inherit attributes and methods from another class.
    2. Reduces code duplication and promotes reusability.
    3. Parent class (superclass) and child class (subclass).
    Polymorphism
    1. Supports method overriding and overloading.
    2. Enables one interface to be used for multiple underlying data types.
    Encapsulation
    1. Binds data and methods together within a class.
    2. Controls access to internal data through public and private methods.
    3. Protects data from unauthorized access.
    Abstraction
    1. Hides complex implementation details.
    2. Focuses on what an object does rather than how it does it.

Creating Classes and Objects

  • We use class keyword to define a class (name in CameCase).
  • Attribute is defined inside __init__ method (constructor) and is accessed using self.
  • Example:
    1. class Car: def __init__(self, brand, model, color="blue"): self.brand = brand self.model = model self.color = color def display_info(self): return f"{self.brand} {self.model} in {self.color}" # Creating an object of Car class my_car = Car("Toyota", "Corolla", "Red") # Constructir automatically called print(my_car.display_info()) # Output: Toyota Corolla in Red

Inheritance

  • Allows a new class to inherit attributes and methods from an existing class.
  • Type: Single (A => B), Multiple ([A, B] => C), Multilevel (A => B => C).
  • Example:
    1. class People: def __init__(self, name, dob): self.name = name self.dob = datetime.strptime(dob, "%Y-%m-%d").date() @property def age(self): return (date.today() - self.dob).days // 365 def is_adult(self): return self.age >= 18 class Employee(People): def __init__(self, name, dob, employee_id, department): super().__init__(name, dob) self.employee_id = employee_id self.department = department # Creating an object of Employee class employee_1 = Employee("Rabindra", "1993-01-01", "E001", "IT") print(employee_1.name) # Inherited from People class print(employee_1.age) # Inherited from People class print(employee_1.employee_id) # Defined in Employee class print(employee_1.is_adult()) # Defined in Employee class

Polymorphism

  • Allows objects of different classes to be treated as objects of a common superclass.
  • Example:
    1. class Nepali: def __init__(self, name): self.name = name def greet(self): return f"Namaste, {self.name}!" class English: def __init__(self, name): self.name = name def greet(self): return f"Hello, {self.name}!" # Polymorphism in action nepali_person = Nepali("Rabindra") english_person = English("John") print(nepali_person.greet()) # Output: Namaste, Rabindra! print(english_person.greet()) # Output: Hello, John!

Method Overriding

  • Allows a subclass to redefine method of a superclass.
  • Example:
    1. class Animal: def speak(self): return "Animal speaks" class Dog(Animal): def speak(self): return "Woof!" # Method overriding in action my_dog = Dog() my_animal = Animal() print(my_dog.speak()) # Output: Woof! print(my_animal.speak()) # Output: Animal speaks

Operator Overloading

  • Allows us to define custom behavior for operators when applied to objects of a class.
  • + adds when applied on number but concates when applied on string.
  • Example:
    1. class Vector2D: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Vector2D(self.x + other.x, self.y + other.y) def __repr__(self): return f"Vector2D({self.x}, {self.y})" # Operator overloading in action v1 = Vector2D(1, 2) v2 = Vector2D(3, 4) v3 = v1 + v2 # Uses the __add__ method print(v3) # Output: Vector2D(4, 6)

Operation Notation

OperatorNameInternal Notation
+Addition__add__
-Subtraction__sub__
*Multiplication__mul__
/Division__truediv__
//Floor Division__floordiv__
%Modulus__mod__
==Equality__eq__
!=Inequality__ne__
<Less Than__lt__
>Greater Than__gt__
<=Less Than or Equal To__le__
>=Greater Than or Equal To__ge__
** Exponent__pow__
print( )Print Function__str__ or __repr__
len( )Length Function__len__
Common Operators and their Internal Notation

Encapsulation

  • Bundles data and methods that operate on that data within a single unit (class).
  • Controls access to internal data through public and private methods.
  • Private attributes names are prefixed by __, method is by _.
  • Private attributes and methods are not accessible from outside the class.
  • Data is protected from unauthorized access and modification.
  • Name Mangling Technique can be used to access private attributes outside the class.
  • Example:
    1. class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.__balance = balance # Private attribute def deposit(self, amount): if amount > 0: self.__balance += amount else: print("Deposit amount must be positive") def withdraw(self, amount): if 0 < amount <= self.__balance: self.__balance -= amount else: print("Invalid withdrawal amount") def get_balance(self): return self.__balance # Public method to access private attribute # Encapsulation in action account = BankAccount("Rabindra", 1000) account.deposit(500) account.withdraw(200) print(account.get_balance()) # Output: 1300 print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'

Abstraction

  • Hides complex implementation details and shows only essential features of an object.
  • Allows users to interact with objects through a simplified interface.
  • Promotes modularity and maintainability.
  • Example: When we drive a car, we interact with the steering wheel, pedals, and gear shift without needing to understand the complex mechanics of how the engine works.
  • We know .lower() method converts string to lowercase but we don't need to know how it works internally to use it.