OOP In Python
Published: 2023-02-15 12:17:07

# OOP In Python
+ OOP (Object Oriented Programming) in Python allows us to use objects to represent entities.
+ Each object has its own attributes and methods derived from the Class it belongs to.
+ Some examples of pre-existing classes in Python are Integer, Strings, Lists, Sets, Dictionary, etc.
+ Whenever we are creating, for example, a new list in Python, we are actually creating an object of class List.
+ The new list created has all the methods that are defined in List Class, for example, append, insert, sort, etc.
+ We can create our own classes and objects in Python to build real-life applications
+ Using OOPS allows us to organize, maintain and scale our code more efficiently.
+ The four main pillars of OOP are Encapsulation, Inheritance, Abstraction, and Polymorphism.
<hr>

## Class and Objects
+ An object is to represent an entity belonging to a particular Class.
+ A class is a blueprint for creating objects in OOP.
+ Classes have their own attributes and methods(functions) that can be accessed by objects belonging to that class.
+ We can initialize the attributes of Class using __init__ method (constructor).
  
```py
class Dog:                          # class naming with PascalCase
    # constructor
    def __init__(self, name, age):
        self.name = name            # creating class attributes
        self.age = age

    def speak(self):                # creating class methods
        return f'My name is {self.name}, and I am {self.age} years old.'

bruno = Dog("Bruno", 9)             # creating object

print(bruno.name)                   # accessing Class attribute from object
print(bruno.age)

print(bruno.speak())                # accessing Class method from object
```
**Output:**

0. Bruno
1. 9
2. My name is Bruno, and I am 9 years old.
<hr>

## 1. Encapsulation:

+ Encapsulation allows us to hide the object's attributes and method from outside access.
+ Python doesn't have encapsulation in the same sense as other OOP languages.
+ Python doesn't have truly private or protected attributes and methods.
+ Attributes and methods that are meant to be protected are prefixed with a single underscore: _
+ However, they can still be accessed and modified using their names.
+ Attributes and methods that are meant to be private are prefixed with a double underscore: __
+ However, they can still be accessed and modified using name mangling.
+ Also, both can still be accessed and modified using get and set methods.

```py
class Dog:

    def __init__(self, name, age):
        self.__name = name      # private attribute
        self._age = age         # protected attribute

    def get_name(self):         # get method
        return self.__name
    
    def set_name(self, name):   # set method
        self.__name = name

    def get_age(self):          # get method
        return self._age
    
    def set_age(self, age):     # set method
        self._age = age

    def __show(self):           # private method
        return f"My name is {self.__name} and I am {self._age} years old."
    
    def _speak(self):           # protected method
        return "WOOF!"

bruno = Dog("Bruno", 9)

bruno._Dog__name = "Shero"  # modifying private attribute using name mangling
print(bruno._Dog__name)     # accessing private attribute using name mangling

bruno.set_name("Cheems")    # modifying private attribute using set method
print(bruno.get_name())     # accessing private attribute using get method

bruno._age = 6              # modifying protected attribute using its name
print(bruno._age)           # accessing protected attribute using its name

bruno.set_age(3)            # modifying protected attribute using set method
print(bruno.get_age())      # accessing protected attribute using get method

print(bruno._Dog__show())   # accessing private method using name mangling
print(bruno._speak())       # accessing protected method using it's name
```

**Output:**

0. Shero
1. Cheems
2. 6
3. 3
4. My name is Cheems and I am 3 years old.
5. WOOF!
<hr>

## 2. Inheritance

+ Inheritance allows us to create a new class (called 'child class') based on another class (called 'parent class').
+ The child class inherits all the attributes and methods of the parent class.
+ In addition, the child class it's own unique attributes and methods.
+ There are several types of inheritance namely, single-level, multi-level, multiple, hierarchical, and hybrid. 

```py
class Pet:

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

    def show(self):
        print(f'My name is {self.name}, and I am {self.age} years old.')

class Cat(Pet):						# inheriting parent class in child class

    def speak(self):				# accessing parent class attribute in child class by default
        print("Meow!")

class Dog(Pet):						# inheriting parent class in child class

    def __init__(self, name, age, sport):
        super().__init__(name, age)	    # accessing parent class attribute in child class using super
        self.sport = sport			    # child class accessing its own attributes

    def speak(self):
        print(f'My favorite sport is {self.sport}.')

D = Dog("Bruno", 9, 'Football')
C = Cat("Missa", 6)

D.show()						# accessing parent class method in child class
D.speak()						# child class accessing its own attributes

C.show()						# accessing parent class method in child class
C.speak()						# child class accessing its own attributes
			
```

**Output:**

0. My name is Bruno, and I am 9 years old.
1. My favorite sport is Football.
2. My name is Missa, and I am 6 years old.
3. Meow!
<hr>

## 3. Abstraction
+ Abstraction allows us to hide classes and methods not necessary for the user.
+ We can make an abstract class in python by setting the parent class of any class to ABC after importing it from abc (Abstract Class Bases).
+ After that, we can create an abstract method using the @abstractmethod decorator.
+ We can still access the non-abstract method of abstract classes.

```py
from abc import ABC, abstractmethod

class PET(ABC):			# create an abstract class
    @abstractmethod		# create an abstract method
    def speak(self):
        pass

    def show(self):
        print("I am a Pet.")

class Dog(PET):

    def speak(self):
        print("WOOF!")

class Cat(PET):

    def speak(self):
        print("Meow!")

bruno = Dog()			
bruno.speak()			# ignoring abstract method of abstract class
bruno.show()			# non-abstract method of the abstract class still works

missa = Cat()			
missa.speak()			# ignoring abstract method of abstract class
missa.show()			# non-abstract method of the abstract class still works

nemo = PET()			# will throw an error because we cannot instantiate an abstract class.
```

**Output:**

0. WOOF!
1. I am a Pet.
2. Meow!
3. I am a Pet.
4. Can't instantiate abstract class PET with abstract method speak
<hr>

## 4. Polymorphism

+ Polymorphism allows an object to take on many forms.
+ Polymorphism in Python can be achieved in two ways: method over-riding and duck-typing.

### A. Method Over-riding
Method overriding allows a child class to have a different definition of a method already defined in the parent class.

```py
class Pet:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        print(f'My name is {self.name} and I am {self.age} years old.')

class Fish(Pet):
    pass

class Dog(Pet):
    
    def speak(self):				# over-riding parent's class method
        print(f'WOOF! My name is {self.name} and I am {self.age} years old.')

nemo = Fish("Nemo", 2)
bruno = Dog("Bruno", 9)

nemo.speak()						# parent's class method runs
bruno.speak()						# child's class method runs
```

**Output:**

0. My name is Nemo and I am 2 years old.
1. WOOF! My name is Bruno and I am 9 years old.

### B. Duck Typing
Duck Typing allows different types of classes to have the same method name with its own definition.

```py
class Fish:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def speak(self):				# method of first class
        print(f'My name is {self.name} and I am {self.age} years old.')

class Dog:

    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed

    def speak(self):				# method of second class with same name
        print(f'WOOF! My name is {self.name} and I am {self.age} years old, and I am a {self.breed}.')

nemo = Fish("Nemo", 2)
bruno = Dog("Bruno", 9, "Shiba Inu")

nemo.speak()						# method of first class runs
bruno.speak()						# method of the second class with the same name runs
```

**Output:**

0. My name is Nemo and I am 2 years old.
2. WOOF! My name is Bruno and I am 9 years old, and I am a Shiba Inu.
<hr>

## Dunder/Magic Methods:
+ Dunder or magic methods are special methods used in Python classes.
+ They are used to define how objects of classes behave in case they are used with in-built Python operations.
+ Some commonly used dunder methods are __init__(self,...), __str__(self), __repr__(self), __len__(self), __ads__(self), __del__(self), etc.
+ Dunder methods cannot be called, they run automatically when called by in-built python functions.
  
```py
class SimpleClass:

    # constructor
    def __init__(self, value1, value2):
        self.value1 = value1
        self.value2 = value2

    # string representation of the object
    def __str__(self):
        return f'value 1 is {self.value1} and value 2 is {self.value2}.'

    # string representation for debugging purposes
    def __repr__(self):
        return f'This is an object of the class SimpleClass'

    # returns output when len() function is called
    def __len__(self):
        return 1

    # returns the sum when + operator is called between two objects.
    def __add__(self, other):
        return (self.value1 + other.value1, self.value2 + other.value2)

    def __del__(self):
        print("object deleted")

a = SimpleClass(5, 10)      # __init__ is called
b = SimpleClass(7, 9)       # __init__ is called

print(a)                    # __str__ is called
print(repr(a))              # __repr__ is called
print(len(a))               # __len__ is called
print(a + b)                # __add_ is called
```

**Output:**

0. value 1 is 5 and value 2 is 10.
1. This is an object of the class SimpleClass
2. 1
3. (12, 19)

### **REFERENCES**

+ [Python Documentation](https://docs.python.org/3/tutorial/classes.html)
+ [YouTube Lecture by CampusX](https://www.youtube.com/watch?v=Mf2RdpEiXjU)